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: 8.3.1) and please state chapter and paragraph name or number you're referring to.
All received mail is processed conscientiously, and received suggestions for improvements will usually have been 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 8, and having shown several examples of other overloaded operators as well (i.e., the insertion and extraction operators in chapters 3 and 6), we will 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 will be 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:
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.5).
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[](int index) const;
delete[] data
.
swap
, cf. chapter 8) 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 will 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 this section we will show how the
insertion operator can be overloaded allowing the insertion of any type of
object, say Person
(see chapter 8), into an ostream
. Having
defined such an overloaded operator the following will be possible:
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
10.12). 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
will solve the problem.
X::operator std::string const() const
then cout << X()
won't
compile. The reason for this is explained in section 20.8, 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
will wait 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
, a
size_t
and an istream
have been defined for PersonData
and since
the assignment operator expects a PersonData
right-hand side (rhs)
argument the compiler will first convert 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 will 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 const operator++(int);It may be implemented as follows:
Unsigned const 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 const 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 postfix increment or decrement operator
using its full member function name then any int
argument passed to the
member function will result in calling the postfix 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 arithmetic assignment variant (like the +=
operator). Whereas the plain binary operators return const expression values,
the arithmetic assignment operators return a (non-const) 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 ("prefix " + s3) += "postfix"; // 5
// 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.
// 5
is interesting in that it should not compile (it
does compile with the
Gnu
compiler). It should not compile, as the +
operator should return a const
string. This should prevent its
modification by the subsequent +=
operator (compare this situation with
int
values: (3 + intVar) += 4
won't compile. Clearly, std::string
does not do `as the ints do'). In the C++ Annotations binary arithmetic
operators return const
values to render statements like statement // 5
incompilable.
Consider the following code, in which a class Binary
supports
an overloaded operator+
:
class Binary { public: Binary(); Binary(int value); Binary const 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 10.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 ASCII-Z
string 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
means that plain binary arithmetic operators should be declared as
free operators, also called free
functions. Functions like the plain binary arithmetic operators conceptually
belong to the class for which they implement the binary operator. Consequently
they should be declared within the class's header file. We will 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 const operator+(Binary const &l_hand, Binary const &r_hand);
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 const operator+(A const &a, B const &b); B const 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)
will allow the compiler to resolve
the ambiguity to the first overloaded +
operator.
The next step is to implement the corresponding
overloaded arithmetic assignment 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 arithmetic assignment operator should
return a reference to the object to which the arithmetic 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 arithmetic assignment operator:
class Binary { public: Binary(); Binary(int value); Binary const operator+(Binary const &rvalue); Binary &operator+=(Binary const &other); }; Binary const operator+(Binary const &lhs, Binary const &rhs);If a class benefits from being move-aware then for each
const
reference parameter an overloaded function expecting a const
rvalue
reference may be considered. With the class Binary
and its operator+
this would boil down to the following overloaded operator+
functions:
Binary const operator+(Binary const &<mp, Binary const &rhs); Binary const operator+(Binary const &lhs, Binary const &&rtmp); Binary const operator+(Binary const &<mp, Binary const &&rtmp);
When implementing the arithmetic assignment operator the strong guarantee should again be kept in mind. Use a temporary object and swap if the arithmetic operation might throw. Example:
Binary &operator+=(Binary const &other) { Binary tmp(*this); tmp.add(other); // this may throw swap(tmp); return *this; }Here, too, defining
operator+=(Binary const &&tmp)
should be
considered if Binary
must be a move-aware class.
Having available the arithmetic assignment operator, implementating
the plain binary operator is easy. 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 may be
contracted into one single return statement, but then compilers usually aren't
able to apply copy elision resulting in two copy constructions. Usually
copy elision is performed when the steps are taken separately:
class Binary { public: Binary(); Binary(int value); Binary const operator+(Binary const &rvalue); Binary &operator+=(Binary const &other); }; Binary const operator+(Binary const &lhs, Binary const &rhs) { Binary tmp(lhs); tmp += rhs; return tmp; }
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 10.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
will initialize 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 = reinterpret_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 11). 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 will be 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
10.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 11).
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
will be 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 will be used
automatically 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[]
will 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
11). Example:
// declaration: void String::operator delete[](void *p, ostream &out); // usage: String *xp = new String[3]; String::operator delete[](xp, cout);
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 will be
called 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 will be 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: " << *reinterpret_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 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' have addresses. CompareFunction
could be
implemented like this:
int compareFunction(void const *p1, void const *p2) { return *reinterpret_cast<Person const *>(p1) // lsearch wants 0 != // for equal objects *reinterpret_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 will set 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 will have 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 ostream &(*func)(ostream &str)) { return (*func)(*this); }In addition to the above overloaded
operator
<< another one is defined
ostream &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 will call the functions, save their return values, and will then
use 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 creaes 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 abovementioned problem. Merely passing its
address is fine, but then no arguments can't 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 will reply 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 stronly 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
will perform the default converstion from enum values to int
values and
thus will apply 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.
+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= *= /= %= ^= &= |= <<= >>= [] () -> ->* 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 10.3.
Finally, the following operators cannot be overloaded:
. .* :: ?: sizeof typeid