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: 7.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 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 7, and having shown several examples of other overloaded operators as well (i.e., the insertion and extraction operators in chapters 3 and 5), we will now take a look at several other interesting examples of operator overloading.
int
s. Indexing the array elements occurs with
the standard array operator []
, but additionally the class checks for
boundary overflow. Furthermore, the
index operator (
operator[]()
) is
interesting in that it both produces a value and accepts a value, when
used, respectively, as a
right-hand value (
rvalue) and a
left-hand value (
lvalue) in expressions.
Here is an example showing the 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] << endl; }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 will also produce a run-time
error as the non-existing value x[20]
is addressed. The IntArray
class
interface is:
class IntArray { int *d_data; unsigned d_size; public: IntArray(unsigned size = 1); IntArray(IntArray const &other); ~IntArray(); IntArray const &operator=(IntArray const &other); // overloaded index operators: int &operator[](unsigned index); // first int const &operator[](unsigned index) const; // second private: void boundary(unsigned index) const; void copy(IntArray const &other); int &operatorIndex(unsigned 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 has
as its prototype a function that returns a reference to an int
. This
allows us to use expressions like x[10]
as rvalues or
lvalues.
We can therefore use the same function to retrieve and to assign values.
Furthermore note that the return value of the overloaded array operator is
not an int const &
, but rather an int &
. In this situation we
don't use const
, as we must be able to change the element we want to
access, when the operator is used as an lvalue.
However, 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
a const object, which cannot be modified. The compiler detects this and
will refuse to compile this object definition if only the first overloaded
index operator is available. Hence the second overloaded index operator. 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 not used with non-const
objects, but it's
only used with const
objects. It is used for
value retrieval, not for
value assignment, but that is precisely what we want using const
objects. Here, members are overloaded only by their const
attribute. This
form of function overloading was introduced earlier in the Annotations
(sections 2.5.11 and 6.2).
Also note that, since the values stored in the IntArray
are primitive
values of type int
, it's OK to use
value return types. 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. The second overloaded index operator would
then use the following prototype:
int IntArray::operator[](int index) const;
delete data
. Therefore, our
standard destroy()
function was not used.
#include "intarray.ih" IntArray::IntArray(unsigned size) : d_size(size) { if (d_size < 1) { cerr << "IntArray: size of array must be >= 1\n"; exit(1); } d_data = new int[d_size]; } IntArray::IntArray(IntArray const &other) { copy(other); } IntArray::~IntArray() { delete[] d_data; } IntArray const &IntArray::operator=(IntArray const &other) { if (this != &other) { delete[] d_data; copy(other); } return *this; } void IntArray::copy(IntArray const &other) { d_size = other.d_size; d_data = new int[d_size]; memcpy(d_data, other.d_data, d_size * sizeof(int)); } int &IntArray::operatorIndex(unsigned index) const { boundary(index); return d_data[index]; } int &IntArray::operator[](unsigned index) { return operatorIndex(index); } int const &IntArray::operator[](unsigned index) const { return operatorIndex(index); } void IntArray::boundary(unsigned index) const { if (index >= d_size) { cerr << "IntArray: boundary overflow, index = " << index << ", should range from 0 to " << d_size - 1 << endl; exit(1); } }Especially note the implementation of the
operator[]()
functions: 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, we could implement both operator[]
members
inline using an auxiliary function int &operatorIndex(size_t index)
const
. It is interesting to note that a const
member function may return
a non-const reference (or pointer) return value, referring to one of the data
members of its object. This is a potentially dangerous backdoor breaking data
hiding. However, as the members in the public interface prevents this breach,
we feel confident in defining int &operatorIndex() const
as a private
function, knowing that it won't be used for this unwanted purpose.
cout
and
cerr
and the
insertion operator (<<). Adapting a class in such a way that the
istream
's
extraction operator (>>) can be used, is implemented
similarly and is simply shown in an example.
The implementation of an overloaded operator<<()
in the context of
cout
or cerr
involves their class, which is
ostream
. This class is
declared in the header file
ostream
and defines only overloaded operator
functions for `basic' types, such as, int
, char *
, etc.. The purpose
of this section is to show how an
insertion operator can be overloaded in
such a way that an object of any class, say Person
(see chapter
7), can be inserted into an ostream
. Having made available 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 << endl;The statement
cout
<< kr
involves operator
<<()
.
This member function has two operands: an ostream &
and a Person
&
. The proposed action is defined in an
overloaded global operator
operator
<<()
expecting two arguments:
// assume declared in `person.h' ostream &operator<<(ostream &, Person const &); // define in some source file ostream &operator<<(ostream &stream, Person const &pers) { return stream << "Name: " << pers.name() << "Address: " << pers.address() << "Phone: " << pers.phone(); }Note the following characteristics of
operator
<<()
:
ostream
object,
to enable `chaining' of the insertion operator.
operator
<<()
act as arguments of the
the overloaded function. In the earlier example, the parameter stream
is
initialized by cout
, the parameter pers
is initialized by kr
.
In order to overload the extraction operator for, e.g., the Person
class, members are needed to modify the private data members. Such
modifiers are normally included in the class interface. For the
Person
class, the following members should be added to the class
interface:
void setName(char const *name); void setAddress(char const *address); void setPhone(char const *phone);The implementation of these members could be straightforward: 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 could also check the reasonableness of the new address. This elaboration, however, is not further pursued here. Instead, let's have a look at the final overloaded extraction operator (>>). A simple implementation is:
istream &operator>>(istream &str, Person &p) { string name; string address; string phone; if (str >> name >> address >> phone) // extract three strings { p.setName(name.c_str()); p.setAddress(address.c_str()); p.setPhon(phone.c_str()); } return str; }Note the stepwise approach that is followed with the extraction operator: first the required information is extracted using available extraction operators (like a
string
-extraction), then, if that succeeds,
modifier members are used to modify the data members of the object to be
extracted. Finally, the stream object itself is returned as a reference.
String
was 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 from 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 includes the
null-pointer. An assignment like stringObject = 0
is perfectly in order.).
Usually, in classes that are less directly coupled to their data than this
String
class, there will be an
accessor member function, like 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, we would have the
following interface for StringArray
:
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); };Using the
StringArray::operator[]
, assignments between the
String
elements can simply be implemented:
StringArray sa(10); sa[4] = sa[3]; // String to String assignmentIt 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]
can receive its new value.
char const *
that's stored in sa[3]
? We try the following code:
char const *cp = sa[3];This, however, won't work: 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
9.12). Furthermore, casting won't work: the compiler
doesn't know how to
cast a String
to a char const *
. How to
proceed from here?
The naive solution is to resort to the accessor member function
c_str()
:
cp = sa[3].c_str()That solution would work, but it looks so 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. Using a conversion
operator a String
object may be interpreted as a char const *
, which
can then be assigned to another char const *
. Conversion operators can be
implemented for all types for which a conversion is needed.
In the current example, the class String
would need a conversion
operator for a char const *
. In class interfaces, the general form of a
conversion operator is:
operator <type>();In our
String
class, this would become:
operator char const *();The implementation of the conversion operator is straightforward:
String::operator char const *() { return d_string; }Notes:
operator
keyword.
cout.form("%s", sa[3])the compiler is confused: are we going to pass a
String &
or a
char const *
to the form()
member function? To help the compiler,
we supply an
static_cast:
cout.form("%s", static_cast<char const *>(sa[3]));
One might wonder what will happen if an object for which, e.g., a
string
conversion operator
is defined is inserted into,
e.g., an ostream
object, into which string
objects can be inserted. In
this case, the compiler will not look for appropriate conversion operators
(like operator string()
), but will report an error. For example, the
following example produces a compilation error:
#include <iostream> #include <string> using namespace std; class NoInsertion { public: operator string() const; }; int main() { NoInsertion object; cout << object << endl; }The problem is caused by the fact that the compiler notices an insertion, applied to an object. It will now look for an appropriate overloaded version of the insertion operator. As it can't find one, it reports a compilation error, instead of performing a two-stage insertion: first using the
operator
string()
insertion, followed by the insertion of that string
into the
ostream
object.
Conversion operators are used when the compiler is given no choice: an
assignment of a NoInsertion
object to a string
object is such a
situation. The problem of how to insert an object into, e.g., an ostream
is simply solved: by defining an appropriate overloaded insertion operator,
rather than by resorting to a conversion operator.
Several considerations apply to conversion operators:
operator
bool()
, allowing constructions like if (cin)
.
operator=(int)
:
#include <iostream> class Lvalue { int d_value; public: operator int&(); }; inline Lvalue::operator int&() { return d_value; } int main() { Lvalue lvalue; lvalue = 5; // won't compile: no lvalue::operator=(int) };
const
member functions
if they don't modify their object's data members.
std::string
's copy constructor is not called. It would have been
called if the conversion operator had been declared as operator string()
:
#include <string> class XString { std::string d_s; public: operator std::string const &() const; }; inline XString::operator std::string const &() const { return d_s; } int main() { XString x; std::string s; s = x; };
Consider the class Person
introduced in chapter 7. This
class has a constructor
Person(char const *name, char const *address, char const *phone)This constructor could be given default argument values:
Person(char const *name, char const *address = "<unknown>", char const *phone = "<unknown>");In several situations this constructor might be used intentionally, possibly providing the default
<unknown>
texts for the address and phone
numbers. For example:
Person frank("Frank", "Room 113", "050 363 9281");Also, functions might use
Person
objects as parameters, e.g., the
following member in a fictitious class PersonData
could be available:
PersonData &PersonData::operator+=(Person const &person);Now, combining the above two pieces of code, we might, do something like
PersonData dbase; dbase += frank; // add frank to the databaseSo far, so good. However, since the
Person
constructor can also be
used as a conversion operator, it is also possible to do:
dbase += "karel";Here, the
char const *
text `karel
' is converted to an (anonymous)
Person
object using the abovementioned Person
constructor: the second
and third parameters use their
default values. Here, an
implicit conversion is performed from a char const *
to a
Person
object, which might not be what the programmer had in mind when the
class Person
was constructed.
As another example, consider the situation where a class representing a
container is constructed. Let's assume that the initial construction of
objects of this class is rather complex and time-consuming, but expanding
an object so that it can accomodate more elements is even more
time-consuming. Such a situation might arise when a hash-table is initially
constructed to contain n
elements: that's OK as long as the table is not
full, but when the table must be expanded, all its elements normally must be
rehashed to allow for the new table size.
Such a class could (partially) be defined as follows:
class HashTable { size_t d_maxsize; public: HashTable(size_t n); // n: initial table size size_t size(); // returns current # of elements // add new key and value void add(std::string const &key, std::string const &value); };Now consider the following implementation of
add()
:
void HashTable::add(string const &key, string const &value) { if (size() > d_maxsize * 0.75) // table gets rather full *this = size() * 2; // Oops: not what we want! // etc. }In the first line of the body of
add()
the programmer first determines
how full the hashtable currently is: if it's more than three quarter full,
then the intention is to double the size of the hashtable. Although this
succeeds, the hashtable will completely fail to fulfill its purpose:
accidentally the programmer assigns an size_t value, intending to tell the
hashtable what its new size should be. This results in the following unwelcome
surprise:
operator=(size_t newsize)
is
available for HashTable
.
HashTable
as its right-hand operand.
HashTable
) is obtained by
(implicitly) constructing an (empty) HashTable
that can accomodate
size() * 2
elements.
HashTable
is thereupon assigned to the
current HashTable
, thus removing all hitherto stored elements from the
current HashTable
.
explicit
modifier with the constructor. Constructors using the explicit
modifier
can only be used for the
explicit construction of objects, and cannot be
used as implicit type convertors anymore. For example, to prevent the implicit
conversion from size_t
to HashTable
the class interface of the
class HashTable
should declare the constructor
explicit HashTable(size_t n);Now the compiler will catch the error in the compilation of
HashTable::add()
, producing an error message like
error: no match for 'operator=' in '*this = (this->HashTable::size()() * 2)'
operator++()
) and
decrement operator (
operator
--()
) creates a little problem: there
are two version 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 rvalue, which is an expression having a fixed value: the post-incremented variable itself disappears from view. Used as prefix operator, the variable is incremented, and its value is returned as lvalue, so it can be altered immediately again. 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. The class could have the following (partially shown) interface:
class Unsigned { size_t d_value; public: Unsigned(); Unsigned(size_t init); Unsigned &operator++(); }This defines the prefix overloaded increment operator. An lvalue is returned, as we can deduce from the return type, which is
Unsigned &
.
The implementation of the above function could be:
Unsigned &Unsigned::operator++() { ++d_value; return *this; }In order to define the postfix operator, an overloaded version of the operator is defined, expecting an
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
wrapper class,
add the following line to the class interface:
Unsigned const operator++(int);The implementation of the postfix increment operator should be like this:
Unsigned const Unsigned::operator++(int) { return d_value++; }The simplicity of this implementation is deceiving. Note that:
d_value
is used with a postfix increment in the
return
expression. Therefore, the value of the return
expression is d_value
's
value, before it is incremented; which is correct.
Unsigned
value. This
anonymous object is implicitly initialized by the value of d_value
, so
there is a
hidden constructor call here.
PersonData
,
mentioned in section 9.4. Presumably, the PersonData
class
contains a complex inner
data organization. If the PersonData
class
would maintain a pointer Person *current
to the Person
object that is
currently selected, then the postfix increment operator for the class
PersonData
could be implemented as follows:
PersonData PersonData::operator++(int) { PersonData tmp(*this); incrementCurrent(); // increment `current', somehow. return tmp; }A matter of concern here could be that this operation actually requires two calls to the copy constructor: first to keep the current state, then to copy the
tmp
object to the (anonymous) return value. In some cases this
double call of the
copy constructor might be
avoidable, by defining a specialized constructor. E.g.,
PersonData PersonData::operator++(int) { return PersonData(*this, incrementCurrent()); }Here,
incrementCurrent()
is supposed to return the information which
allows the constructor to set its current
data member to the pre-increment
value, at the same time incrementing current
of the actual
PersonData
object. The above constructor would have to:
this
object.
current
based on the return value of its second
parameter, which could be, e.g., an index.
incrementCurrent()
would have incremented
current
of the actual PersonData
object.
The general rule is that double calls of the copy constructor can be avoided if a specialized constructor can be defined initializing an object to the pre-increment state of the current object. The current object itself has its necessary data members incremented by a function, whose return value is passed as argument to the constructor, thereby informing the constructor of the pre-incremented state of the involved data members. The postfix increment operator will then return the thus constructed (anonymous) object, and no copy constructor is ever called.
Finally it is noted that the call of the increment or decrement operator
using
its overloaded
function name might require us to provide an (any) int
argument to inform
the compiler that we want the postfix increment function. E.g.,
PersonData p; p = other.operator++(); // incrementing `other', then assigning `p' p = other.operator++(0); // assigning `p', then incrementing `other'
operator+()
) can
be a very natural extension of the class's functionality. For example, the
std::string
class has various overloaded forms of operator+()
as have
most abstract containers, covered in chapter 12.
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 (annotated 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
should not compile (although it does compile with
the
Gnu
compiler version 3.1.1). It should not compile, as the +
operator should return a const
string, thereby preventing its modification
by the subsequent +=
operator. Below we will consequently follow this line
of reasoning, and will ensure that overloaded binary operators will always
return const
values.
Now 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, the notion of a
promotion is
introduced. As we have seen in section 9.4, constructors requiring
a single argument may be implicitly activated when an object is apparently
initialized by an argument of a corresponding type. We've encountered this
repeatedly with std::string
objects, when an ASCII-Z
string was used
to initialize a std::string
object.
In situations where a member function expects a const &
to an object
of its own class (like the Binary const &
that was specified in the
declaration of the Binary::operator+()
member mentioned above), the type
of the actually used argument may also be any type that can be used as an
argument for a single-argument constructor of that class. This implicit call
of a constructor to obtain an object of the proper type is called a
promotion.
So, 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.
Note that no promotions are possibly in statement // 2
: here the +
operator is applied to an int
typed value, which has no concept of a
`constructor', `member function' or `promotion'.
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 binary operators are declared as
classless functions, also
called
free functions. However, they conceptually belong to the class for
which they implement the binary operator, and so they should be declared in
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 &l_hand, Binary const &r_hand);
Finally, having available the arithmetic assignment operator, the
implementation of the plain binary operator turns out to be extremely
simple. It contains of a single return statement, in which an anonymous object
is constructed to which the arithmetic assignment operator is applied. This
anonymous object is then returned by the plain binary operator as its
const
return value. Since its implementation consists of merely one
statement it is usually provided in-line, adding to its efficiency:
class Binary { public: Binary(); Binary(int value); Binary const operator+(Binary const &rvalue); Binary &operator+=(Binary const &other); }; Binary const operator+(Binary const &l_hand, Binary const &r_hand) { return Binary(l_hand) += r_hand; }One might wonder where the temporary value is located. Most compilers apply in these cases a procedure called ` return value optimization': the anonymous object is created at the location where the eventual returned object will be stored. So, rather than first creating a separate temporary object, and then copying this object later on to the return value, it initializes the return value using the
l_hand
argument, and then applies the +=
operator to add the r_hand
argument to it. Without return value
optimization it would have to:
l_hand
r_hand
to it
operator new
is overloaded, it must have a
void *
return type,
and at least an argument of type
size_t
. The size_t
type is defined in
the header file
cstddef
, which must therefore be included when the
operator
new
is overloaded.
It is also possible to define multiple versions of the operator new
, as
long as each version has its own unique set of arguments. The global new
operator can still be used, through the
::
-operator. If a class X
overloads the operator new
, then the system-provided operator new
is
activated by
X *x = ::new X();Overloading
new[]
is discussed in section 9.9.
The following example shows an overloaded version of operator new
:
#include <cstddef> void *X::operator new(size_t sizeofX) { void *p = new char[sizeofX]; return memset(p, 0, sizeof(X)); }Now, let's see what happens when
operator new
is overloaded for the
class X
. Assume that class is defined as
follows (For the sake of simplicity we have violated the principle
of
encapsulation here. The principle of encapsulation, however, is
immaterial to the discussion of the workings of the operator new
.):
class X { public: void *operator new(size_t sizeofX); int d_x; int d_y; };Now, consider the following program fragment:
#include "x.h" // class X interface #include <iostream> using namespace std; int main() { X *x = new X(); cout << x->d_x << ", " << x->d_y << endl; }This small program produces the following output:
0, 0At the call of
new X()
, our little program performed the following
actions:
operator new
was called, which allocated and initialized
a block of memory, the size of an X
object.
X()
constructor. Since no constructor was defined,
the constructor itself didn't do anything at all.
operator new
the allocated X
object was already initialized to zeros when the
constructor was called.
Non-static member functions are passed a (hidden) pointer to the object on
which they should operate. This
hidden pointer becomes the
this
pointer
in
non-static member functions. This procedure is also followed for
constructors. In the next pieces of pseudo C++ code, the pointer is made
visible. In the first part an X
object x
is defined directly, in the
second part of the example the (overloaded) operator new
is used:
X::X(&x); // x's address is passed to the // constructor void *ptr = X::operator new(); // new allocates the memory X::X(ptr); // next the constructor operates on the // memory returned by 'operator new'Notice that in the pseudo
C++
fragment the member functions were
treated as static member function of the class X
. Actually, operator
new
is a
static member function of its class: it cannot reach data
members of its object, since it's normally the task of the operator new
to create room for that object. It can do that by allocating enough
memory, and by initializing the area as required. Next, the memory is passed
(as the this
pointer) to the constructor for further processing. The
fact that an overloaded operator new
is actually a static function, not
requiring an object of its class, can be illustrated in the following (frowned
upon in normal situations!) program fragment, which can be compiled without
problems (assume class X
has been defined and is available as before):
int main() { X x; X::operator new(sizeof x); }The call to
X::operator new()
returns a void *
to an initialized block
of memory, the size of an X
object.
The operator new
can have multiple parameters. The first parameter is
initialized by an
implicit argument and is always the
size_t
parameter,
other parameters are initialized by
explicit arguments that are specified
when operator new
is used. For example:
class X { public: void *operator new(size_t p1, size_t p2); void *operator new(size_t p1, char const *fmt, ...); }; int main() { X *p1 = new(12) X(), *p2 = new("%d %d", 12, 13) X(), *p3 = new("%d", 12) X(); }The pointer
p1
is a pointer to an X
object for which the memory
has been allocated by the call to the first overloaded operator new
,
followed by the call of the constructor X()
for that block of memory. The
pointer p2
is a pointer to an X
object for which the memory has been
allocated by the call to the second overloaded operator new
, followed
again by a call of the constructor X()
for its block of memory. Notice
that pointer p3
also uses the second overloaded operator new()
, as
that
overloaded operator accepts a
variable number of arguments, the
first of which is a char const *
.
Finally note that no explicit argument is passed for new
's first
parameter, as this argument is implicitly provided by the type specification
that's required for operator new
.
delete
operator may be overloaded too. The
operator delete
must
have a
void *
argument, and an optional second argument of type
size_t
,
which is the size in bytes of objects of the class for which the operator
delete
is overloaded. The return type of the overloaded operator delete
is
void
.
Therefore, in a class the operator delete
may be overloaded using the
following prototype:
void operator delete(void *);or
void operator delete(void *, size_t);Overloading
delete[]
is discussed in section 9.9.
The `home-made' operator delete
is called after executing the
destructor of the associated class. So, the statement
delete ptr;with
ptr
being a pointer to an object of the class X
for which the
operator delete
was overloaded, boils down to the following statements:
X::~X(ptr); // call the destructor function itself // and do things with the memory pointed to by ptr X::operator delete(ptr, sizeof(*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 activated using the
::
scope resolution operator. For example:
void X::operator delete(void *ptr) { // any operation considered necessary, then: ::delete ptr; }
operator new[]
and
operator delete[]
were introduced. Like
operator new
and
operator delete
the
operators new[]
and delete[]
may be overloaded. Because 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 be followed:
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.
The default way these operators act is 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 an object, the
destructor of its class is
called. operator new[]
is used to allocate a series of primitive values
or objects. Note that if a series of objects is allocated, the class's
default constructor is called to initialize each individual
object. 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. However, if
pointers to objects were allocated, no destructor is called, as a
pointer is considered a primitive type, and certainly not an object.
new[]
and delete[]
may only be overloaded in
classes. Consequently, when allocating primitive types or
pointers to objects only the default line of action is followed: when arrays
of pointers to objects are deleted, a
memory leak occurs unless the objects
to which the pointers point were deleted earlier.
In this section the mere syntax for overloading operators
new[]
and delete[]
is presented. It is left as an
exercise to the reader to make good use of these overloaded operators.
operator new[]
in a class Object
the interface should
contain the following lines, showing multiple forms of overloaded forms of
operator new[]
:
class Object { public: void *operator new[](size_t size); void *operator new[](size_t index, size_t extra); };The first form shows the basic form of
operator new[]
. It
should return a
void *
, and defines at least a
size_t
parameter. When
operator new[]
is called, size
contains the number of bytes that
must be allocated for the required number of objects. These objects can be
initialized by the
global operator new[] using the form
::new Object[size / sizeof(Object)]Or, alternatively, the required (uninitialized) amount of memory can be allocated using:
::new char[size]An example of an overloaded operator
new[]
member function, returning an
array of Object
objects all filled with 0-bytes, is:
void *Object::operator new[](size_t size) { return memset(new char[size], 0, size); }Having constructed the overloaded operator
new[]
, it will be used
automatically in statements like:
Object *op = new Object[12];Operator
new[]
may be overloaded using additional parameters. The
second form of the overloaded operator new[]
shows such an additional
size_t
parameter. The definition of such a function is standard, and
could be:
void *Object::operator new[](size_t size, size_t extra) { size_t n = size / sizeof(Object); Object *op = ::new Object[n]; for (size_t idx = 0; idx < n; idx++) op[idx].value = extra; // assume a member `value' return op; }To use this overloaded operator, only the additional parameter must be provided. It is given in a parameter list just after the name of the operator itself:
Object *op = new(100) Object[12];This results in an array of 12
Object
objects, all having their
value
members set to 100.
operator new[]
operator delete[]
may be overloaded.
To overload operator delete[]
in a class Object
the interface should
contain the following lines, showing multiple forms of overloaded forms of
operator delete[]
:
class Object { public: void operator delete[](void *p); void operator delete[](void *p, size_t index); void operator delete[](void *p, int extra, bool yes); };
The first form shows the basic form of operator delete[]
. Its
parameter is initialized to the address of a block of memory previously
allocated by Object::new[]
. These objects can be
deleted by the
global operator delete[] using the form
::delete[]
. However, the compiler expects ::delete[]
to receive a
pointer to Objects
, so a
type cast is necessary:
::delete[] reinterpret_cast<Object *>(p);An example of an overloaded operator
delete[]
is:
void Object::operator delete[](void *p) { cout << "operator delete[] for Objects called\n"; ::delete[] reinterpret_cast<Object *>(p); }Having constructed the overloaded operator
delete[]
, it will be used
automatically in statements like:
delete[] new Object[5];
Operator delete[]
may be overloaded using additional
parameters. However, if overloaded as
void operator delete[](void *p, size_t size);then
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 the
first form should not be defined, to avoid
ambiguity. An example of this form of
operator delete[]
is:
void Object::operator delete[](void *p, size_t size) { cout << "deleting " << size << " bytes\n"; ::delete[] reinterpret_cast<Object *>(p); }
If additional parameters are defined, as in
void operator delete[](void *p, int extra, bool yes);an explicit argument list must be provided. With
delete[]
, the
argument list is specified following the brackets:
delete[](new Object[5], 100, false);
operator()()
. By defining the function
call operator an object masquerades as a function, hence the term
function objects.
Function objects play an important role in generic algorithms and their use is preferred over alternatives like pointers to functions. The fact that they are important in the context of generic algorithms leaves us in a didactic dilemma: at this point it would have been nice if generic algorithms would 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.
Function objects are objects for which operator()()
has been
defined. Function objects are commonly used in combination with generic
algorithms, but also in situations where otherwise pointers to
functions would have been used. Another reason for using function objects is
to support
inline
functions, which cannot be used in combination with
pointers to functions.
An important set of functions and function objects is the set of
predicate functions and function objects. The return value of a
predicate function or of the function call operator of a predicate function
object is true
or false
. Both predicate functions and predicate
function objects are commonly referred to as `predicates'. Predicates are
frequently used by generic algorithms. E.g., the count_if
generic algorithm, covered in chapter 17, returns the number of times
the function object that's passed to it returns 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. A program
fragment using this function is:
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()
is called to determine the person we're
looking for, and the function 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 the lsearch()
function. It could be something like:
int compareFunction(Person const *p1, Person const *p2) { return *p1 != *p2; // lsearch() wants 0 for equal objects }This, of course, assumes that the
operator!=()
has been overloaded in
the class Person
, as it is quite unlikely that a
bytewise comparison
will be appropriate here. But overloading operator!=()
is no big deal, so
let's assume that that operator is available as well.
With lsearch()
(and friends, having parameters that are
pointers to functions) an
inline compare function cannot be used:
as the address of the compare()
function must be known to the
lsearch()
function. So, on average n / 2
times at least the
following actions take place:
lsearch()
is determined,
producing the address of compareFunction()
;
Person::operator!=()
argument is pushed on the stack;
Person::operator!=()
function is evaluated;
Person::operator!=()
function is popped off
the stack again;
PersonSearch()
, having the following prototype
(this, however, is not the preferred approach. Normally a
generic algorithm will be preferred to a home-made function. But for now
our PersonSearch()
function is used 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, nothing much has been altered. We've replaced the call to
lsearch()
with a call to another function: PersonSearch()
. Now we
show what happens inside PersonSearch()
:
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; }The implementation shows a plain linear search. However, in the for-loop the expression
target(base[idx])
shows our target
object
used as a function object. Its implementation can be simple:
bool Person::operator()(Person const &other) const { return *this != other; }Note the somewhat peculiar syntax:
operator()()
. The first set
of parentheses define the particular operator that is overloaded: the function
call operator. The second set of parentheses define the parameters that are
required for this function. Operator()()
appears in the class header
file as:
bool operator()(Person const &other) const;Now,
Person::operator()()
is a simple function. It contains but one
statement, so we could consider making it
inline. Assuming that we do, than
this is what happens when operator()()
is called:
Person::operator!=()
argument is pushed on the stack,
operator!=()
function is evaluated,
Person::operator!=()
argument is popped off the
stack,
operator()()
is an inline function, it
is not actually called. Instead operator!=()
is called immediately. Also
note that the required
stack operations are fairly modest.
So, function objects may be defined inline. This is not possible for functions that are called indirectly (i.e., using pointers to functions). Therefore, even if the function object needs to do very little work it has to be defined as an ordinary function if it is going to be called via pointers. The overhead of performing the indirect call may annihilate the advantage of the flexibility of calling functions indirectly. In these cases function objects that are defined as inline functions can result in an increase of efficiency of the program.
Finally, function objects may access the private data of their objects
directly. 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. When function objects
are used, the target person doesn't vary within a single search
task. Therefore, the target person could be passed to the constructor of the
function object doing the comparison. This is in fact what happened in the
expression target(base[idx])
, where only one argument is passed to the
operator()()
member function of the target
function object.
As noted, function objects play a central role in generic algorithms. In chapter 17 these generic algorithms are discussed in detail. Furthermore, in that chapter predefined function objects will be introduced, further emphasizing the importance of the function object concept.
cout
<< hex
<< 13
<< endl
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 to 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 a 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" << endl; cout << "And " << w10 << 3 << " more ships sailed too." << endl; }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); }
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 unwanted 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
<<()
calls the compiler
will first call the functions, and then use their return values in the
insertion sequence. That will invalidate 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. The problem now is that it
isn't clear how the function will receive its arguments: you can't just call
it, since that produces the abovementioned problem, and you can't just pass
its address in the insertion sequence, as you normally do with a
manipulator.
However, there is 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" << "'" << endl; } /* 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 the following
code fragment can be used:
fstream out; out.open("/tmp/out", ios::out);Combinations are possible as well. 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, though. Consider the following code snippet:
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 the following:
invalid conversion from 'int' to 'Permission'
The question is of course: why is it OK to combine ios::openmode
values and pass these combined values to the stream's open()
member, but
is it not OK to combine Permission
values.
Combining enum values using arithmetic operators results in int
-typed
values. On the other hand, conceptually this might not be the
intention. Conceptually it can be correct to combine enum values with the
resulting value still conceptually within the original enumeration
domain. Note that even adding a value READWRITE = READ | WRITE
to the
above enum
will not allow us to specify READ | WRITE
as an argument to
setPermission()
.
To answer the question about how to combine 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, but even those overloads
were conceptually within the domain of some class. C++, however, is a
stronly typed language and defining an enum
is really something more than
just associating int
-values with symbolic names. An enumeration type is a
real type, 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
this as it is offered no alternative. But by overloading operators for
enum-type values we can 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>(left | right); }Other operators can easily be conveived of where applicable.
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 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 9.3.
Finally, the following operators are not overloadable at all:
. .* :: ?: sizeof typeid