Don't hesitate to send in feedback: send an e-mail if you like the C++ Annotations; if you think that important material was omitted; if you find errors or typos in the text or the code examples; or if you just feel like e-mailing. Send your e-mail to Frank B. Brokken.Please state the document version you're referring to, as found in the title (in this document: 10.3.0) and please state chapter and paragraph name or number you're referring to.
All received mail is processed conscientiously, and received suggestions for improvements are usually processed by the time a new version of the Annotations is released. Except for the incidental case I will normally not acknowledge the receipt of suggestions for improvements. Please don't interpret this as me not appreciating your efforts.
Having covered the overloaded assignment operator in chapter 9, and having shown several examples of other overloaded operators as well (i.e., the insertion and extraction operators in chapters 3 and 6), we now take a look at operator overloading in general.
IntArray
encapsulating an array of int
s. Indexing the array elements is possible
using the standard array index operator []
, but additionally checks for
array bounds overflow are performed. Furthermore, the
index operator (operator[]
) is interesting in that it can be used in
expressions as both lvalue and as rvalue.
Here is an example showing the basic use of the class:
int main() { IntArray x(20); // 20 ints for (int i = 0; i < 20; i++) x[i] = i * 2; // assign the elements for (int i = 0; i <= 20; i++) // produces boundary overflow cout << "At index " << i << ": value is " << x[i] << '\n'; }
First, the constructor is used to create an object containing 20
int
s. The elements stored in the object can be assigned or retrieved. The
first for
-loop assigns values to the elements using the index operator,
the second for
-loop retrieves the values but also results in a
run-time error once the non-existing value x[20]
is addressed. The
IntArray
class interface is:
#include <cstddef> class IntArray { int *d_data; size_t d_size; public: IntArray(size_t size = 1); IntArray(IntArray const &other); ~IntArray(); IntArray const &operator=(IntArray const &other); // overloaded index operators: int &operator[](size_t index); // first int const &operator[](size_t index) const; // second void swap(IntArray &other); // trivial private: void boundary(size_t index) const; int &operatorIndex(size_t index) const; };
This class has the following characteristics:
size_t
parameter having a
default argument value, specifying the number of int
elements in the
object.
The first overloaded index operator allows us to reach and modify the
elements of non-constant IntArray
objects. This overloaded operator's
prototype is a function returning a reference to an int
. This allows
us to use expressions like x[10]
as rvalues or lvalues.
With non-const IntArray
objects operator[]
can therefore be used
to retrieve and to assign values. The return value of the non-const
operator[]
member is not an int const &
, but an int &
. In this
situation we don't use const
, as we must be able to modify the element we
want to access when the operator is used as lvalue.
This whole scheme fails if there's nothing to assign. Consider the
situation where we have an IntArray const stable(5)
. Such an object is an
immutable const object. The compiler detects this and refuses to compile
this object definition if only the non-const operator[]
is
available. Hence the second overloaded index operator is added to the class's
interface. Here the return value is an int const &
, rather than an int
&
, and the member function itself is a const
member function. This second
form of the overloaded index operator is only used with const
objects. It
is used for value retrieval instead of value assignment. That, of course,
is precisely what we want when using const
objects. In this situation
members are overloaded only by their const
attribute. This form of
function overloading was introduced earlier in the C++ Annotations (sections
2.5.4 and 7.7).
Since IntArray
stores values of a primitive type IntArray
's
operator[] const
could also have defined a value return type. However,
with objects one usually doesn't want the extra copying that's implied with
value return types. In those cases const &
return values are preferred for
const
member functions. So, in the IntArray
class an int
return
value could have been used as well, resulting in the following prototype:
int IntArray::operator[](size_t index) const;
delete[] data
.
swap
, cf. chapter 9) are:
#include "intarray.ih" IntArray::IntArray(size_t size) : d_size(size) { if (d_size < 1) throw string("IntArray: size of array must be >= 1"); d_data = new int[d_size]; } IntArray::IntArray(IntArray const &other) : d_size(other.d_size), d_data(new int[d_size]) { memcpy(d_data, other.d_data, d_size * sizeof(int)); } IntArray::~IntArray() { delete[] d_data; } IntArray const &IntArray::operator=(IntArray const &other) { IntArray tmp(other); swap(tmp); return *this; } int &IntArray::operatorIndex(size_t index) const { boundary(index); return d_data[index]; } int &IntArray::operator[](size_t index) { return operatorIndex(index); } int const &IntArray::operator[](size_t index) const { return operatorIndex(index); } void IntArray::boundary(size_t index) const { if (index < d_size) return; ostringstream out; out << "IntArray: boundary overflow, index = " << index << ", should be < " << d_size << '\n'; throw out.str(); }
Note how the operator[]
members were implemented: as non-const members
may call const member functions and as the implementation of the const
member function is identical to the non-const member function's implementation
both operator[]
members could be defined inline using an auxiliary
function int &operatorIndex(size_t index) const
. A const
member
function may return a non-const reference (or pointer) return value, referring
to one of the data members of its object. Of course, this is a potentially
dangerous backdoor that may break data hiding. However, the members in the
public interface prevent this breach and so the two public operator[]
members may themselves safely call the same int &operatorIndex() const
member, that defines a
private backdoor.
std::ostream
and
std::istream
.
The class std::ostream
defines insertion operators for primitive
types, such as int
, char *
, etc.. In this section we learn how to
extend the existing functionality of classes (in particular std::istream
and std::ostream
) in such a way that they can be used also in combination
with classes developed much later in history.
In particular we will show how the
insertion operator can be overloaded allowing the insertion of any type of
object, say Person
(see chapter 9), into an ostream
. Having
defined such an overloaded operator we're able to use the following code:
Person kr("Kernighan and Ritchie", "unknown", "unknown"); cout << "Name, address and phone number of Person kr:\n" << kr << '\n';The statement
cout
<< kr
uses operator
<<. This member
function has two operands: an ostream &
and a Person &
. The required
action is defined in an overloaded free function operator
<< expecting two
arguments:
// declared in `person.h' std::ostream &operator<<(std::ostream &out, Person const &person); // defined in some source file ostream &operator<<(ostream &out, Person const &person) { return out << "Name: " << person.name() << ", " "Address: " << person.address() << ", " "Phone: " << person.phone(); }The free function
operator
<< has the following noteworthy characteristics:
ostream
object,
to enable `chaining' of the insertion operator.
operator
<< are passed to the free function as its
arguments. In the example, the parameter out
was initialized by cout
,
the parameter person
by kr
.
In order to overload the extraction operator for, e.g., the Person
class, members are needed modifying the class's private data members. Such
modifiers are normally offered by the class interface. For
the Person
class these members could be the following:
void setName(char const *name); void setAddress(char const *address); void setPhone(char const *phone);These members may easily be implemented: the memory pointed to by the corresponding data member must be deleted, and the data member should point to a copy of the text pointed to by the parameter. E.g.,
void Person::setAddress(char const *address) { delete[] d_address; d_address = strdupnew(address); }A more elaborate function should check the reasonableness of the new address (
address
also shouldn't be a 0-pointer). This
however, is not further pursued here. Instead, let's have a look at the final
operator
>>. A simple implementation is:
istream &operator>>(istream &in, Person &person) { string name; string address; string phone; if (in >> name >> address >> phone) // extract three strings { person.setName(name.c_str()); person.setAddress(address.c_str()); person.setPhone(phone.c_str()); } return in; }Note the stepwise approach that is followed here. First, the required information is extracted using available extraction operators. Then, if that succeeds, modifiers are used to modify the data members of the object to be extracted. Finally, the stream object itself is returned as a reference.
String
, constructed around the char *
type. Such a class may define
all kinds of operations, like assignments. Take a look at the following class
interface, designed after the string
class:
class String { char *d_string; public: String(); String(char const *arg); ~String(); String(String const &other); String const &operator=(String const &rvalue); String const &operator=(char const *rvalue); };Objects of this class can be initialized from a
char const *
, and also
from a String
itself. There is an overloaded assignment operator, allowing
the assignment from a String
object and from a char const
*
(Note that the assignment from a char const *
also allows the
null-pointer. An assignment like stringObject = 0
is perfectly in order.).
Usually, in classes that are less directly linked to their data than this
String
class, there will be an accessor member function,
like a member char const *String::c_str() const
. However, the need to use
this latter member doesn't appeal to our intuition when an array of String
objects is defined by, e.g., a class StringArray
. If this latter class
provides the operator[]
to access individual String
members, it would
most likely offer at least the following class interface:
class StringArray { String *d_store; size_t d_n; public: StringArray(size_t size); StringArray(StringArray const &other); StringArray const &operator=(StringArray const &rvalue); ~StringArray(); String &operator[](size_t index); };This interface allows us to assign
String
elements to each other:
StringArray sa(10); sa[4] = sa[3]; // String to String assignmentBut it is also possible to assign a
char const *
to an element of
sa
:
sa[3] = "hello world";Here, the following steps are taken:
sa[3]
is evaluated. This results in a String
reference.
String
class is inspected for an overloaded assignment,
expecting a char const *
to its right-hand side. This operator is
found, and the string object sa[3]
receives its new value.
char const *
that's stored in sa[3]
? The following attempt fails:
char const *cp = sa[3];It fails since we would need an overloaded assignment operator for the 'class
char const *
'. Unfortunately, there isn't such a class, and
therefore we can't build that overloaded assignment operator (see also section
11.13). Furthermore, casting won't work as the
compiler doesn't know how to cast a String
to a char const *
. How
to proceed?
One possibility is to define an accessor member function c_str()
:
char const *cp = sa[3].c_str()This compiles fine but looks clumsy.... A far better approach would be to use a conversion operator.
A conversion operator is a kind of overloaded operator, but this time the overloading is used to cast the object to another type. In class interfaces, the general form of a conversion operator is:
operator <type>() const;Conversion operators usually are
const
member functions: they are
automatically called when their objects are used as rvalues in expressions
having a type
lvalue. Using a conversion operator a String
object may be interpreted as a char const *
rvalue, allowing us to perform
the above assignment.
Conversion operators are somewhat dangerous. The conversion is automatically
performed by the compiler and unless its use is perfectly transparent it may
confuse those who read code in which conversion operators are used. E.g.,
novice C++ programmers are frequently confused by statements like `if
(cin) ...
'.
As a rule of thumb: classes should define at most one conversion
operator. Multiple conversion operators may be defined but frequently result
in ambiguous code. E.g., if a class defines operator bool() const
and
operator int() const
then passing an object of this class to a function
expecting a size_t
argument results in an ambiguity as an int
and a
bool
may both be used to initialize a size_t
.
In the current example, the class String
could define the following
conversion operator for char const *
:
String::operator char const *() const { return d_string; }Notes:
operator
keyword.
String
argument is passed to
a function specifying an ellipsis parameter) the compiler needs a hand to
disambiguate our intentions. A static_cast
solves the problem.
X::operator std::string const() const
then cout << X()
won't
compile. The reason for this is explained in section 21.9, but a
shortcut allowing the conversion operator to work is to define the following
overloaded operator<<
function:
std::ostream &operator<<(std::ostream &out, std::string const &str) { return out.write(str.data(), str.length()); }
X
defining a conversion operator also defines an
insertion operator accepting an X
object the insertion operator is used;
Insertable
is directly
inserted; an object of the class Convertor
uses the conversion operator;
an object of the class Error
cannot be inserted since it does not define
an insertion operator and the type returned by its conversion operator cannot
be inserted either (Text
does define an operator int() const
, but
the fact that a Text
itself cannot be inserted causes the error):
#include <iostream> #include <string> using namespace std; struct Insertable { operator int() const { cout << "op int()\n"; } }; ostream &operator<<(ostream &out, Insertable const &ins) { return out << "insertion operator"; } struct Convertor { operator Insertable() const { return Insertable(); } }; struct Text { operator int() const { return 1; } }; struct Error { operator Text() const { return Text(); } }; int main() { Insertable insertable; cout << insertable << '\n'; Convertor convertor; cout << convertor << '\n'; Error error; cout << error << '\n'; }
Some final remarks regarding conversion operators:
operator
bool()
, allowing constructions like if (cin)
.
operator int &()
conversion operator) opens up a back door, and the
operator can only be used as lvalue when explicitly called (as in:
x.operator int&() = 5
). Don't use it.
const
member functions
as they don't modify their object's data members.
Assume a data base class DataBase
is defined in which Person
objects can be stored. It defines a Person *d_data
pointer, and so it
offers a copy constructor and an overloaded assignment operator.
In addition to the copy constructor DataBase
offers a default
constructor and several additional constructors:
DataBase(Person const &)
: the DataBase
initially contains a
single Person
object;
DataBase(istream &in)
: the data about multiple persons are read from
in
.
DataBase(size_t count, istream &in = cin)
: the data of count
persons are read from in
, by default the standard input stream.
The above constructors all are perfectly reasonable. But they also allow the compiler to compile the following code without producing any warning at all:
DataBase db; DataBase db2; Person person; db2 = db; // 1 db2 = person; // 2 db2 = 10; // 3 db2 = cin; // 4Statement 1 is perfectly reasonable:
db
is used to redefine
db2
. Statement 2 might be understandable since we designed DataBase
to
contain Person
objects. Nevertheless, we might question the logic that's
used here as a Person
is not some kind of DataBase
. The logic becomes
even more opaque when looking at statements 3 and 4. Statement 3 in effect
waits for the data of 10 persons to appear at the standard input
stream. Nothing like that is suggested by db2 = 10
.
All four statements are the result of implicit
promotions. Since constructors accepting, respectively a Person
, an
istream
, and a size_t
and an istream
have been defined for
DataBase
and since the assignment operator expects a DataBase
right-hand side (rhs) argument the compiler first converts the rhs arguments
to anonymous DataBase
objects which are then assigned to db2
.
It is good practice to prevent implicit promotions by using the
explicit
modifier when declaring a constructor. Constructors using the
explicit
modifier can only be used to construct objects
explicitly. Statements 2-4 would not have compiled if the constructors
expecting one argument would have been declared using explicit
. E.g.,
explicit DataBase(Person const &person); explicit DataBase(size_t count, std:istream &in);
Having declared all constructors accepting one argument as explicit
the above assignments would have required the explicit specification of
the appropriate constructors, thus clarifying the programmer's intent:
DataBase db; DataBase db2; Person person; db2 = db; // 1 db2 = DataBase(person); // 2 db2 = DataBase(10); // 3 db2 = DataBase(cin); // 4As a rule of thumb prefix one argument constructors with the
explicit
keyword unless implicit promotions are perfectly natural
(string
's char const *
accepting constructor is a case in point).
For example, a class might define operator bool() const
returning true
if an object of that class is in a usable state and false
if not.
Since the type bool
is an arithmetic type this could result in unexpected
or unintended behavior. Consider:
class StreamHandler { public: operator bool() const; // true: object is fit for use ... }; int fun(StreamHandler &sh) { int sx; if (sh) // intended use of operator bool() ... use sh as usual; also use `sx' process(sh); // typo: `sx' was intended }In this example
process
unintentionally receives the value returned by
operator bool
using the implicit conversion from bool
to int
.
With explicit
conversion operators implicit conversions like the one shown
in the example are prevented and such conversion operators can only be used
in situations where the converted type is explicitly required. E.g., in the
condition sections of if
or repetition statements where a bool
value
is expected. In such cases an explicit operator bool()
conversion operator
would automatically be used.
operator++
) and
decrement operator (operator
--) introduces a small
problem: there are two versions of each operator, as they may be used as
postfix operator (e.g., x++
) or as prefix operator (e.g.,
++x
).
Used as postfix operator, the value's object is returned as an rvalue, temporary const object and the post-incremented variable itself disappears from view. Used as prefix operator, the variable is incremented, and its value is returned as lvalue and it may be altered again by modifying the prefix operator's return value. Whereas these characteristics are not required when the operator is overloaded, it is strongly advised to implement these characteristics in any overloaded increment or decrement operator.
Suppose we define a wrapper class around the size_t
value
type. Such a class could offer the following (partially shown) interface:
class Unsigned { size_t d_value; public: Unsigned(); explicit Unsigned(size_t init); Unsigned &operator++(); }The class's last member declares the prefix overloaded increment operator. The returned lvalue is
Unsigned &
. The member is easily
implemented:
Unsigned &Unsigned::operator++() { ++d_value; return *this; }
To define the postfix operator, an overloaded version of the operator
is defined, expecting a (dummy) int
argument. This might be considered a
kludge, or an acceptable application of function overloading. Whatever
your opinion in this matter, the following can be concluded:
Unsigned
's interface:
Unsigned operator++(int);It may be implemented as follows:
Unsigned Unsigned::operator++(int) { Unsigned tmp(*this); ++d_value; return tmp; }Note that the operator's parameter is not used. It is only part of the implementation to disambiguate the prefix- and postfix operators in implementations and declarations.
In the above example the statement incrementing the current object offers
the nothrow guarantee as it only involves an operation on a
primitive type. If the initial copy construction throws then the original
object is not modified, if the return statement throws the object has safely
been modified. But incrementing an object could itself throw exceptions. How
to implement the increment operators in that case? Once again, swap
is our
friend. Here are the pre- and postfix operators offering the strong guarantee
when the member increment
performing the increment operation may throw:
Unsigned &Unsigned::operator++() { Unsigned tmp(*this); tmp.increment(); swap(tmp); return *this; } Unsigned Unsigned::operator++(int) { Unsigned tmp(*this); tmp.increment(); swap(tmp); return tmp; }The postfix increment operator first creates a copy of the current object. That copy is incremented and then swapped with the current object. If
increment
throws the current object remains unaltered; the swap operation
ensures that the original object is returned and the current object becomes
the incremented object.
When calling the increment or decrement operator using its full member
function name then any int
argument passed to the function results in
calling the postfix operator. Omitting the argument results in calling the
prefix operator. Example:
Unsigned uns(13); uns.operator++(); // prefix-incrementing uns uns.operator++(0); // postfix-incrementing uns
operator+
) can
be a very natural extension of the class's functionality. For example, the
std::string
class has various overloaded forms of operator+
.
Most binary operators come in two flavors: the plain binary operator (like
the +
operator) and the compound assignment variant (like the +=
operator). Whereas the plain binary operators return values,
the compound assignment operators return a reference to the
object to which the operator was applied. For example, with std::string
objects the following code (annotations below the example) may be used:
std::string s1; std::string s2; std::string s3; s1 = s2 += s3; // 1 (s2 += s3) + " postfix"; // 2 s1 = "prefix " + s3; // 3 "prefix " + s3 + "postfix"; // 4
// 1
the contents of s3
is added to s2
. Next, s2
is returned, and its new contents are assigned to s1
. Note that +=
returns s2
itself.
// 2
the contents of s3
is also added to s2
, but as
+=
returns s2
itself, it's possible to add some more to s2
// 3
the +
operator returns a std::string
containing
the concatenation of the text prefix
and the contents of s3
. This
string returned by the +
operator is thereupon assigned to s1
.
// 4
the +
operator is applied twice. The effect is:
+
returns a std::string
containing
the concatenation of the text prefix
and the contents of s3
.
+
operator takes this returned string as its left
hand value, and returns a string containing the concatenated text of its left
and right hand operands.
+
operator represents the
value of the expression.
Consider the following code, in which a class Binary
supports
an overloaded operator+
:
class Binary { public: Binary(); Binary(int value); Binary operator+(Binary const &rvalue); }; int main() { Binary b1; Binary b2(5); b1 = b2 + 3; // 1 b1 = 3 + b2; // 2 }
Compilation of this little program fails for statement // 2
, with the
compiler reporting an error like:
error: no match for 'operator+' in '3 + b2'Why is statement
// 1
compiled correctly whereas statement // 2
won't compile?
In order to understand this remember promotions. As we have seen in
section 11.4, constructors expecting a single argument may be
implicitly activated when an argument of an appropriate type is
provided. We've encountered this repeatedly with std::string
objects,
where an NTBS may be used to initialize a std::string
object.
Analogously, in statement // 1
, the +
operator is called for the
b2
object. This operator expects another Binary
object as its right
hand operand. However, an int
is provided. As a constructor
Binary(int)
exists, the int
value is first promoted to a Binary
object. Next, this Binary
object is passed as argument to the
operator+
member.
In statement // 2
no promotions are available: here the +
operator
is applied to an lvalue that is an int
. An int
is a primitive type and
primitive types have no concept of `constructors', `member functions' or
`promotions'.
How, then, are promotions of left-hand operands implemented in statements
like "prefix " + s3
? Since promotions are applied to function arguments,
we must make sure that both operands of binary operators are arguments. This
implies that plain binary operators supporting promotions for
either their left-hand side operand or right-hand side operand should be
declared as
free operators,
also called free functions.
Functions like the plain binary operators conceptually belong to the class
for which they implement the binary operator. Consequently they should be
declared in the class's header file. We cover their implementations
shortly, but here is our first revision of the declaration of the class
Binary
, declaring an overloaded +
operator as a free function:
class Binary { public: Binary(); Binary(int value); }; Binary operator+(Binary const &lhs, Binary const &rhs);
By defining binary operators as free functions, the following promotions are possible:
class A; class B { public: B(A const &a); }; class A { public: A(); A(B const &b); }; A operator+(A const &a, B const &b); B operator+(B const &b, A const &a); int main() { A a; a + a; };
Here, both overloaded +
operators are possible when compiling
the statement a + a
. The ambiguity must be solved by explicitly promoting
one of the arguments, e.g., a + B(a)
allows the compiler to resolve
the ambiguity to the first overloaded +
operator.
The next step is to implement the corresponding overloaded binary
assignment operator, having the form @=
, with @
being a binary
operator. As this operator always has a left-hand operand which is an
object of its own class, it is implemented as a true member
function. Furthermore, the compound assignment operator should return a
reference to the object to which the binary operation applies, as the
object might be modified in the same statement. E.g.,
(s2 += s3) + " postfix"
. Here is our second revision of the class
Binary
, showing both the declaration of the plain binary operator and the
corresponding compound assignment operator:
class Binary { public: Binary(); Binary(int value); Binary &operator+=(Binary const &rhs); }; Binary operator+(Binary const &lhs, Binary const &rhs);
How should the compound addition assignment operator be implemented? When
implementing the compound assignment operator the strong guarantee should
again be kept in mind. Use a temporary object and swap if the add
member
might throw. Example:
Binary &operator+=(Binary const &other) { Binary tmp(*this); tmp.add(other); // this may throw swap(tmp); return *this; }
It's easy to implement the plain binary operator for classes offering
the matching compound assignment operator: the lhs
argument is
copied into a Binary tmp
to which the rhs
operand is added. Then
tmp
is returned. The copy construction and two statements could be
contracted into one single return statement, but then compilers usually aren't
able to apply copy elision in this case. But
copy elision is usually used when the steps are taken separately:
class Binary { public: Binary(); Binary(int value); Binary &operator+=(Binary const &other); }; Binary operator+(Binary const &lhs, Binary const &rhs) { Binary tmp(lhs); tmp += rhs; return tmp; }
But wait! Remember the design principle for move-aware classes that was given in section 9.7.8? When implementing binary operators we're doing exactly that what was mentioned in that design principle. A temporay object is constructed and the compound assignment operation is applied to the temporary object.
If the class Binary
is a move-aware class then we can add a move-aware
binary operator to our class at very little cost. The actual work is performed
by the compound addition assignment operator, as described. However, this
operator is called from the move-aware binary operator having prototype
Binary operator+(Binary &<mp, Binary const &rhs);The traditional binary operator's implementation now simply consists of two steps:
Here is the declaration and implementation of the traditional and
move-aware compound addition assignment operator of the class Binary
for
operator+
:
class Binary { public: Binary(); Binary(int value); // copy and move constructors are available by default, or // they can be explicitly declared and implemented. Binary &operator+=(Binary const &other); // see the text }; inline Binary operator+(Binary &&lhs, Binary const &rhs) { return std::move(lhs += rhs); // avoids copy construction } inline Binary operator+(Binary const &lhs, Binary const &rhs) { Binary tmp(lhs); return std::move(tmp) + rhs; }
operator new
is overloaded, it must define a void *
return
type, and its first parameter must be of type size_t
. The default
operator new
defines only one parameter, but overloaded versions may
define multiple parameters. The first one is not explicitly specified but is
deducted from the size of objects of the class for which operator new
is overloaded. In this section overloading operator new
is discussed.
Overloading new[]
is discussed in section 11.9.
It is possible to define multiple versions of the operator new
, as long as
each version defines its own unique set of arguments. When overloaded
operator new
members must dynamically allocate memory they can do so using
the global operator new
, applying the scope resolution operator
::
. In the next example the overloaded operator new
of the class
String
initializes the substrate of dynamically allocated String
objects to 0-bytes:
#include <cstddef> #include <iosfwd> class String { std::string *d_data; public: void *operator new(size_t size) { return memset(::operator new(size), 0, size); } bool empty() const { return d_data == 0; } };The above
operator new
is used in the following program, illustrating
that even though String
's default constructor does nothing the object's
data are initialized to zeroes:
#include "string.h" #include <iostream> using namespace std; int main() { String *sp = new String; cout << boolalpha << sp->empty() << '\n'; // shows: true }At
new String
the following took place:
String::operator new
was called, allocating and
initializing a block of memory, the size of a String
object.
String
constructor. Since no constructor was defined,
the constructor itself didn't do anything at all.
String::operator new
initialized the allocated memory to zero bytes
the allocated String
object's d_data
member had already been
initialized to a 0-pointer by the time it started to exist.
All member functions (including constructors and destructors) we've
encountered so far define a (hidden) pointer to the object on which they
should operate. This hidden pointer becomes the function's this
pointer.
In the next example of pseudo C++ code, the pointer is explicitly
shown to illustrate what's happening when operator new
is used. In the
first part a String
object str
is directly defined, in the second
part of the example the (overloaded) operator new
is used:
String::String(String *const this); // real prototype of the default // constructor String *sp = new String; // This statement is implemented // as follows: String *sp = static_cast<String *>( // allocation String::operator new(sizeof(String)) ); String::String(sp); // initializationIn the above fragment the member functions were treated as object-less member functions of the class
String
. Such members are
called static member functions (cf. chapter 8). Actually,
operator new
is such a static member function. Since it has no
this
pointer it cannot reach data members of the object for which it is
expected to make memory available. It can only allocate and initialize the
allocated memory, but cannot reach the object's data members by name as there
is as yet no data object layout defined.
Following the allocation, the memory is passed (as the this
pointer)
to the constructor for further processing.
Operator new
can have multiple parameters. The first parameter is
initialized as an implicit argument and is always a size_t
parameter. Additional overloaded operators may define additional
parameters. An interesting additional operator new
is the
placement new operator. With the placement new
operator a block of memory has already been set aside and one of the class's
constructors is used to initialize that memory. Overloading placement new
requires an operator new
having two parameters: size_t
and char *
,
pointing to the memory that was already available. The size_t
parameter is implicitly initialized, but the remaining parameters must
explicitly be initialized using arguments to operator new
. Hence we reach
the familiar syntactical form of the placement new operator in use:
char buffer[sizeof(String)]; // predefined memory String *sp = new(buffer) String; // placement new callThe declaration of the placement new operator in our class
String
looks like this:
void *operator new(size_t size, char *memory);It could be implemented like this (also initializing the
String
's
memory to 0-bytes):
void *String::operator new(size_t size, char *memory) { return memset(memory, 0, size); }Any other overloaded version of
operator new
could also be
defined. Here is an example showing the use and definition of an overloaded
operator new
storing the object's address immediately in an existing array
of pointers to String
objects (assuming the array is large enough):
// use: String *next(String **pointers, size_t *idx) { return new(pointers, (*idx)++) String; } // implementation: void *String::operator new(size_t size, String **pointers, size_t idx) { return pointers[idx] = ::operator new(size); }
delete
operator may also be overloaded. In fact it's good practice to
overload operator delete
whenever operator new
is also overloaded.
Operator delete
must define a void *
parameter. A second overloaded
version defining a second parameter of type size_t
is related to
overloading operator new[]
and is discussed in section
11.9.
Overloaded operator delete
members return void
.
The `home-made' operator delete
is called when deleting a dynamically
allocated object after executing the destructor of the associated
class. So, the statement
delete ptr;with
ptr
being a pointer to an object of the class String
for
which the operator delete
was overloaded, is a shorthand for the following
statements:
ptr->~String(); // call the class's destructor // and do things with the memory pointed to by ptr String::operator delete(ptr);The overloaded
operator delete
may do whatever it wants to do with the
memory pointed to by ptr
. It could, e.g., simply delete it. If that would
be the preferred thing to do, then the default
delete
operator can be called using the ::
scope resolution operator. For example:
void String::operator delete(void *ptr) { // any operation considered necessary, then, maybe: ::delete ptr; }To declare the above overloaded
operator delete
simply add the
following line to the class's interface:
void operator delete(void *ptr);Like
operator new operator delete
is a static member function
(see also chapter 8).
operator new[]
and operator delete[]
were introduced. Like
operator new
and operator delete
the
operators new[]
and delete[]
may be overloaded.
As it is possible to overload new[]
and delete[]
as well as
operator new
and operator delete
, one should be careful in selecting
the appropriate set of operators. The following rule of thumb should always
be applied:
Ifnew
is used to allocate memory,delete
should be used to deallocate memory. Ifnew[]
is used to allocate memory,delete[]
should be used to deallocate memory.
By default these operators act as follows:
operator new
is used to allocate a single object or
primitive value. With an object, the object's constructor is
called.
operator delete
is used to return the memory allocated by operator
new
. Again, with class-type objects, the class's destructor is
called.
operator new[]
is used to allocate a series of primitive values or
objects. If a series of objects is allocated, the class's default constructor
is called to initialize each object individually.
operator delete[]
is used to delete the memory previously allocated
by new[]
. If objects were previously allocated, then the destructor
is called for each individual object. Be careful, though, when pointers to
objects were allocated. If
pointers to objects were allocated the destructors of the objects to
which the allocated pointers point won't automatically be called. A pointer is
a primitive type and so no further action is taken when it is returned to the
common pool.
operator new[]
in a class (e.g., in the class
String
) add the following line to the class's interface:
void *operator new[](size_t size);The member's
size
parameter is implicitly provided and is initialized
by C++'s run-time system to the amount of memory that must be allocated.
Like the simple one-object operator new
it
should return a void *
. The number of objects that must be initialized can
easily be computed from size / sizeof(String)
(and of course replacing
String
by the appropriate class name when overloading operator new[]
for another class). The overloaded new[]
member may allocate raw memory
using e.g., the default operator new[]
or the default operator new
:
void *operator new[](size_t size) { return ::operator new[](size); // alternatively: // return ::operator new(size); }Before returning the allocated memory the overloaded
operator new[]
has a chance to do something special. It could, e.g., initialize the memory to
zero-bytes.
Once the overloaded operator new[]
has been defined, it is
automatically used in statements like:
String *op = new String[12];Like
operator new
additional overloads of operator new[]
may be
defined. One opportunity for an operator new[]
overload is overloading
placement new specifically for arrays of objects. This operator is
available by default but becomes unavailable once at least one overloaded
operator new[]
is defined. Implementing placement new
is not
difficult. Here is an example, initializing the available memory to 0-bytes
before returning:
void *String::operator new[](size_t size, char *memory) { return memset(memory, 0, size); }To use this overloaded operator, the second parameter must again be provided, as in:
char buffer[12 * sizeof(String)]; String *sp = new(buffer) String[12];
operator delete[]
in a class String
add the following line
to the class's interface:
void operator delete[](void *memory);Its parameter is initialized to the address of a block of memory previously allocated by
String::new[]
.
There are some subtleties to be aware of when implementing
operator delete[]
. Although the addresses returned by new
and
new[]
point to the allocated object(s), there is an additional size_t
value available immediately before the address returned by new
and
new[]
. This size_t
value is part of the allocated block and contains
the actual size of the block. This of course does not hold true for the
placement new
operator.
When a class defines a destructor the size_t
value preceding the
address returned by new[]
does not contain the size of the allocated
block, but the number of objects specified when calling
new[]
. Normally that is of no interest, but when overloading operator
delete[]
it might become a useful piece of information. In those cases
operator delete[]
does not receive the address returned by new[]
but rather the address of the initial size_t
value. Whether this is at all
useful is not clear. By the time delete[]
's code is executed all objects
have already been destroyed, so operator delete[]
is only to determine how
many objects were destroyed but the objects themselves cannot be used anymore.
Here is an example showing this behavior of operator delete[]
for a
minimal Demo
class:
struct Demo { size_t idx; Demo() { cout << "default cons\n"; } ~Demo() { cout << "destructor\n"; } void *operator new[](size_t size) { return ::operator new(size); } void operator delete[](void *vp) { cout << "delete[] for: " << vp << '\n'; ::operator delete[](vp); } }; int main() { Demo *xp; cout << ((int *)(xp = new Demo[3]))[-1] << '\n'; cout << xp << '\n'; cout << "==================\n"; delete[] xp; } // This program displays (your 0x?????? addresses might differ, but // the difference between the two should be sizeof(size_t)): // default cons // default cons // default cons // 3 // 0x8bdd00c // ================== // destructor // destructor // destructor // delete[] for: 0x8bdd008Having overloaded
operator delete[]
for a class String
, it will be
used automatically in statements like:
delete[] new String[5];
Operator delete[]
may also be overloaded using an additional
size_t
parameter:
void operator delete[](void *p, size_t size);Here
size
is automatically initialized to the size (in bytes) of the
block of memory to which void *p
points. If this form is defined, then
void operator[](void *)
should not be defined, to avoid ambiguities.
An example of this latter form of operator delete[]
is:
void String::operator delete[](void *p, size_t size) { cout << "deleting " << size << " bytes\n"; ::operator delete[](ptr); }
Additional overloads of operator delete[]
may be defined, but to use
them they must explicitly be called as static member functions (cf. chapter
8). Example:
// declaration: void String::operator delete[](void *p, ostream &out); // usage: String *xp = new String[3]; String::operator delete[](xp, cout);
operator delete
and operator
delete[]
members.
The C++14 standard also supports overloading the global void operator
delete(void *, size_t size)
and void operator delete[](void *, size_t
size)
functions.
When a global sized deallocation function is defined, it is automatically used instead of the default, non-sized deallocation function. The performance of programs may improve if a sized deallocation function is available (cf. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3663.html).
new[]
expression, what will
happen? In this section we'll show that new[]
is
exception safe even when
only some of the objects were properly constructed.
To begin, new[]
might throw while trying to allocate the required
memory. In this case a bad_alloc
is thrown and we don't leak as nothing
was allocated.
Having allocated the required memory the class's default constructor is going
to be used for each of the objects in turn. At some point a constructor might
throw. What happens next is defined by the C++ standard: the destructors
of the already constructed objects are called and the memory allocated for
the objects themselves is returned to the common pool. Assuming that the
failing constructor offers the basic guarantee new[]
is therefore
exception safe even if a constructor may throw.
The following example illustrates this behavior. A request to allocate and initialize five objects is made, but after constructing two objects construction fails by throwing an exception. The output shows that the destructors of properly constructed objects are called and that the allocated substrate memory is properly returned:
#include <iostream> using namespace std; static size_t count = 0; class X { int x; public: X() { if (count == 2) throw 1; cout << "Object " << ++count << '\n'; } ~X() { cout << "Destroyed " << this << "\n"; } void *operator new[](size_t size) { cout << "Allocating objects: " << size << " bytes\n"; return ::operator new(size); } void operator delete[](void *mem) { cout << "Deleting memory at " << mem << ", containing: " << *static_cast<int *>(mem) << "\n"; ::operator delete(mem); } }; int main() try { X *xp = new X[5]; cout << "Memory at " << xp << '\n'; delete[] xp; } catch (...) { cout << "Caught exception.\n"; } // Output from this program (your 0x??? addresses might differ) // Allocating objects: 24 bytes // Object 1 // Object 2 // Destroyed 0x8428010 // Destroyed 0x842800c // Deleting memory at 0x8428008, containing: 5 // Caught exception.
operator()
. By defining the function call
operator an object masquerades as a function, hence the term
function objects.
Function objects are also known as
functors.
Function objects are important when using generic algorithms. The use of function objects is preferred over alternatives like pointers to functions. The fact that they are important in the context of generic algorithms leaves us with a didactic dilemma. At this point in the C++ Annotations it would have been nice if generic algorithms would already have been covered, but for the discussion of the generic algorithms knowledge of function objects is required. This bootstrapping problem is solved in a well known way: by ignoring the dependency for the time being, for now concentrating on the function object concept.
Function objects are objects for which operator()
has been
defined. Function objects are not just used in combination with generic
algorithms, but also as a (preferred) alternative to pointers to
functions.
Function objects are frequently used to implement
predicate functions. Predicate functions return boolean values.
Predicate functions and predicate function objects are commonly referred to as
`predicates'. Predicates are frequently used by generic algorithms such as the
count_if generic algorithm, covered in chapter 19,
returning the number of times its function object has returned true
. In
the standard template library two kinds of predicates are used: unary
predicates receive one argument, binary predicates receive two arguments.
Assume we have a class Person
and an array of Person
objects. Further
assume that the array is not sorted. A well known procedure for finding a
particular Person
object in the array is to use the function
lsearch
, which performs a lineair search in an array. Example:
Person &target = targetPerson(); // determine the person to find Person *pArray; size_t n = fillPerson(&pArray); cout << "The target person is"; if (!lsearch(&target, pArray, &n, sizeof(Person), compareFunction)) cout << " not"; cout << "found\n";The function
targetPerson
determines the person we're looking for, and
fillPerson
is called to fill the array. Then lsearch
is used to
locate the target person.
The comparison function must be available, as its address is one of the
arguments of lsearch
. It must be a real function having an address. If it
is defined inline then the compiler has no choice but to ignore that request
as inline functions don't have addresses. CompareFunction
could be
implemented like this:
int compareFunction(void const *p1, void const *p2) { return *static_cast<Person const *>(p1) // lsearch wants 0 != // for equal objects *static_cast<Person const *>(p2); }This, of course, assumes that the
operator!=
has been overloaded in
the class Person
. But overloading operator!=
is no big deal, so
let's assume that that operator is actually available.
On average n / 2
times at least the
following actions take place:
lsearch
is determined,
producing compareFunction
's address;
Person::operator!=
argument is pushed on the stack;
Person::operator!=
is evaluated;
Person::operator!=
function is popped off
the stack;
PersonSearch
, having the following prototype (this,
however, is not the preferred approach. Normally a generic algorithm is
preferred over a home-made function. But for now we focus on PersonSearch
to illustrate the use and implementation of a function object):
Person const *PersonSearch(Person *base, size_t nmemb, Person const &target);This function can be used as follows:
Person &target = targetPerson(); Person *pArray; size_t n = fillPerson(&pArray); cout << "The target person is"; if (!PersonSearch(pArray, n, target)) cout << " not"; cout << "found\n";So far, not much has been changed. We've replaced the call to
lsearch
with a call to another function: PersonSearch
. Now look at
PersonSearch
itself:
Person const *PersonSearch(Person *base, size_t nmemb, Person const &target) { for (int idx = 0; idx < nmemb; ++idx) if (target(base[idx])) return base + idx; return 0; }
PersonSearch
implements a plain linear search. However, in the
for-loop we see target(base[idx])
. Here target
is used as a
function object. Its implementation is simple:
bool Person::operator()(Person const &other) const { return *this == other; }Note the somewhat peculiar syntax:
operator()
. The first set of
parentheses define the operator that is overloaded: the function call
operator. The second set of parentheses define the parameters that are
required for this overloaded operator. In the class header file this
overloaded operator is declared as:
bool operator()(Person const &other) const;Clearly
Person::operator()
is a simple function. It contains but one
statement, and we could consider defining it inline. Assuming we do,
then this is what happens when operator()
is called:
Person::operator==
argument is pushed on the stack;
operator==
function is evaluated (which probably also is a
semantic improvement over calling operator!=
when looking for an
object equal to a specified target object);
Person::operator==
argument is popped off the
stack.
operator()
is an inline function, it is not
actually called. Instead operator==
is called immediately. Moreover, the
required stack operations are fairly modest.
Function objects may truly be defined inline. Functions that are called indirectly (i.e., using pointers to functions) can never be defined inline as their addresses must be known. Therefore, even if the function object needs to do very little work it is defined as an ordinary function if it is going to be called through pointers. The overhead of performing the indirect call may annihilate the advantage of the flexibility of calling functions indirectly. In these cases using inline function objects can result in an increase of a program's efficiency.
An added benefit of function objects is that they may access the private data
of their objects. In a search algorithm where a compare function is used (as
with lsearch
) the target and array elements are passed to the compare
function using pointers, involving extra stack handling. Using function
objects, the target person doesn't vary within a single search
task. Therefore, the target person could be passed to the function object's
class constructor. This is in fact what happens in the expression
target(base[idx])
receiving as its only argument the subsequent elements
of the array to search.
cout
<< hex
<< 13
<< to display the value 13 in hexadecimal format. One
may wonder by what magic the hex
manipulator accomplishes this. In this
section the construction of manipulators like hex
is covered.
Actually the construction of a manipulator is rather simple. To start, a
definition of the manipulator is needed. Let's assume we want to create a
manipulator w10
which sets the field width of the next field to be
written by the ostream
object to 10. This manipulator is constructed as a
function. The w10
function needs to know about the ostream
object
in which the width must be set. By providing the function with an ostream
&
parameter, it obtains this knowledge. Now that the function knows about the
ostream
object we're referring to, it can set the width in that object.
Next, it must be possible to use the manipulator in an insertion
sequence. This implies that the return value of the manipulator must be
a reference to an ostream
object also.
From the above considerations we're now able to construct our w10
function:
#include <ostream> #include <iomanip> std::ostream &w10(std::ostream &str) { return str << std::setw(10); }
The w10
function can of course be used in a `stand alone' mode, but it
can also be used as a manipulator. E.g.,
#include <iostream> #include <iomanip> using namespace std; extern ostream &w10(ostream &str); int main() { w10(cout) << 3 << " ships sailed to America\n"; cout << "And " << w10 << 3 << " more ships sailed too.\n"; }
The w10
function can be used as a manipulator because the class
ostream
has an overloaded operator
<< accepting a pointer to a function
expecting an ostream &
and returning an ostream &
. Its definition is:
ostream& operator<<(ostream &(*func)(ostream &str)) { return (*func)(*this); }In addition to the above overloaded
operator
<< another one is defined
ios_base &operator<<(ios_base &(*func)(ios_base &base)) { (*func)(*this); return *this; }This latter function is used when inserting, e.g.,
hex
or
internal
.
The above procedure does not work for manipulators requiring arguments.
It is of course possible to overload operator
<< to accept an ostream
reference and the address of a function expecting an ostream &
and, e.g.,
an int
, but while the address of such a function may be specified with the
<<-operator, the arguments itself cannot be specified. So, one wonders
how the following construction has been implemented:
cout << setprecision(3)In this case the manipulator is defined as a macro. Macro's, however, are the realm of the preprocessor, and may easily suffer from unwelcome side-effects. In C++ programs they should be avoided whenever possible. The following section introduces a way to implement manipulators requiring arguments without resorting to macros, but using anonymous objects.
operator
<< operators in one statement
the compiler calls the functions, saves their return values, and then
uses their return values in the insertion sequence. That invalidates the
ordering of the arguments passed to your <<-operators.
So, one might consider constructing another overloaded operator
<< accepting
the address of a function receiving not just the ostream
reference, but a
series of other arguments as well. But this creates the problem that it isn't
clear how the function should receive its arguments: you can't just call it
since that takes us back to the above-mentioned problem. Merely passing its
address is fine, but then no arguments can be passed to the function.
There exists a solution, based on the use of anonymous objects:
Align
, whose
constructor expects multiple arguments. In our example representing,
respectively, the field width and the alignment.
ostream &operator<<(ostream &ostr, Align const &align)so we can insert an
Align
object into the ostream.
#include <iostream> #include <iomanip> class Align { unsigned d_width; std::ios::fmtflags d_alignment; public: Align(unsigned width, std::ios::fmtflags alignment); std::ostream &operator()(std::ostream &ostr) const; }; Align::Align(unsigned width, std::ios::fmtflags alignment) : d_width(width), d_alignment(alignment) {} std::ostream &Align::operator()(std::ostream &ostr) const { ostr.setf(d_alignment, std::ios::adjustfield); return ostr << std::setw(d_width); } std::ostream &operator<<(std::ostream &ostr, Align const &align) { return align(ostr); } using namespace std; int main() { cout << "`" << Align(5, ios::left) << "hi" << "'" << "`" << Align(10, ios::right) << "there" << "'\n"; } /* Generated output: `hi '` there' */
Note that in order to insert an anonymous Align
object into the
ostream
, the operator
<< function must define a Align const &
parameter (note the const
modifier).
[io]fstream::open
members expect an ios::openmode
value as their
final argument. E.g., to open an fstream
object for writing you could do
as follows:
fstream out; out.open("/tmp/out", ios::out);Combinations are also possible. To open an
fstream
object for
both reading and writing the following stanza is often seen:
fstream out; out.open("/tmp/out", ios::in | ios::out);
When trying to combine enum values using a `home made' enum
we may run
into problems. Consider the following:
enum Permission { READ = 1 << 0, WRITE = 1 << 1, EXECUTE = 1 << 2 }; void setPermission(Permission permission); int main() { setPermission(READ | WRITE); }When offering this little program to the compiler it replies with an error message like this:
invalid conversion from 'int' to 'Permission'
The question is of course: why is it OK to combine ios::openmode
values passing these combined values to the stream's open
member, but
not OK to combine Permission
values.
Combining enum values using arithmetic operators results in int
-typed
values. Conceptually this never was our intention. Conceptually it can be
considered correct to combine enum values if the resulting value conceptually
makes sense as a value that is still within the original enumeration
domain. Note that after adding a value READWRITE = READ | WRITE
to the
above enum
we're still not allowed to specify READ | WRITE
as an
argument to setPermission
.
To answer the question about combining enumeration values and yet stay
within the enumeration's domain we turn to operator overloading. Up to this
point operator overloading has been applied to class types. Free functions
like operator<<
have been overloaded, and those overloads are conceptually
within the domain of their class.
As C++ is a strongly typed language realize that defining an enum
is
really something beyond the mere association of int
-values with symbolic
names. An enumeration type is really a type of its own, and as with any type
its operators can be overloaded. When writing READ | WRITE
the compiler
performs the default conversion from enum values to int
values and
applies the operator to ints
. It does so when it has no alternative.
But it is also possible to overload the enum type's operators. Thus we may ensure that we'll remain within the enum's domain even though the resulting value wasn't defined by the enum. The advantage of type-safety and conceptual clarity is considered to outweigh the somewhat peculiar introduction of values hitherto not defined by the enum.
Here is an example of such an overloaded operator:
Permission operator|(Permission left, Permission right) { return static_cast<Permission>(static_cast<int>(left) | right); }Other operators can easily and analogously be constructed.
Operators like the above were defined for the ios::openmode
enumeration type, allowing us to specify ios::in | ios::out
as argument to
open
while specifying the corresponding parameter as ios::openmode
as well. Clearly, operator overloading can be used in many situations, not
necessarily only involving class-types.
A user-defined literal is defined by a function (see also section 23.3)
that must be defined at namespace scope. Such a function is called a
literal operator.
A literal operator cannot be a class member function. The names of a
literal operator must start with an
underscore, and a literal operator is
used (called) by suffixing its name (including the underscore) to the
argument that must be passed to it . Assuming _NM2km
(nautical mile to km)
is the name of a literal operator, then it could be called as 100_NM2km
,
producing, e.g., the value 185.2.
Using Type
to represent the return type of the literal operator
its generic declaration looks like this:
Type operator "" _identifier(parameter-list);The blank space trailing the empty string is required. The parameter lists of literal operators can be:
unsigned long long int
. It is used as, e.g., 123_identifier
. The
argument to this literal operator can be decimal constants,
binary constants (initial 0b), octal constants (initial 0) and
hexadecimal constants (initial 0x);
long double
. It is used as, e.g., 12.25_NM2km
;
char const *text
. The text
argument is an NTBS. It is
used as, e.g., 1234_pental
. The argument must not be given
double quotes, and must represent a numeric constant, as also expected
by literal operators defining unsigned long long int
parameters.
char const *text, size_t len
. Here, the compiler determines len
as if it had called strlen(text)
. It is used as, e.g.,
"hello"_nVowels
;
wchar_t const *text, size_t len
, same as the previous one, but
accepting a string of wchar_t
characters. It is used as, e.g.,
L"1234"_charSum
;
char16_t const *text, size_t len
, same as the previous one, but
accepting a string of char16_t
characters. It is used as, e.g.,
u"utf 16"_uc
;
char32_t const *text, size_t len
, same as the previous one, but
accepting a string of char32_t
characters. It is used as, e.g.,
U"UTF 32"_lc
;
unsigned long long int
parameter and not by its
overloaded version, defining a char const *
parameter. But if overloaded
literal operators exist defining char const *
and long double
parameters then the operator defining a char const *
parameter is used
when the argument 120 is provided, while the operator defining a long
double
parameter is used with the argument 120.3.
A literator operator can define any return type. Here is
an example of a definition of the _NM2km
literal operator:
double operator "" _NM2km(char const *nm) { return std::stod(nm) * 1.852; } double value = 120_NM2km; // example of useOf course, the argument could also have been a
long double
constant. Here's an alternative implementation, explicitly expecting a long
double
:
double constexpr operator "" _NM2km(long double nm) { return nm * 1.852; } double value = 450.5_NM2km; // example of use
A numeric constant can also be processed completely at compile-time. Section 23.3 provides the details of this type of literal operator.
Arguments to literal operators are themselves always constants. A literal
operator like _NM2km
cannot be used to convert, e.g., the value of a
variable. A literal operator, although it is defined as functinon, cannot be
called like a function. The following examples therefore
result in compilation errors:
double speed; speed_NM2km; // no identifier 'speed_NM2km' _NM2km(speed); // no function _NM2km _NM2km(120.3); // no function _NM2km
+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= *= /= %= ^= &= |= <<= >>= [] () -> ->* new new[] delete delete[]Several operators have textual alternatives:
textual alternative | operator |
and | && |
and_eq | &= |
bitand | & |
bitor | | |
compl | ~ |
not | ! |
not_eq | != |
or | || |
or_eq | |= |
xor | ^ |
xor_eq | ^= |
operator and
). However, note that textual alternatives are not
additional operators. So, within the same context operator&&
and
operator and
can not both be overloaded.
Several of these operators may only be overloaded as member functions
within a class. This
holds true for the '='
, the '[]'
, the '()'
and the '->'
operators. Consequently, it isn't possible to redefine, e.g., the assignment
operator globally in such a way that it accepts a char const *
as an
lvalue
and a String &
as an rvalue. Fortunately, that isn't
necessary either, as we have seen in section 11.3.
Finally, the following operators cannot be overloaded:
. .* :: ?: sizeof typeid