We're always interested in getting feedback. E-mail us if you like this guide, if you think that important material is omitted, if you encounter errors in the code examples or in the documentation, if you find any typos, or generally just if you feel like e-mailing. Send your email to Frank Brokken.Please state the document version you're referring to, as found in the title (in this document: 5.2.0a) and please state the paragraph you're referring to.
All mail received is seriously considered, and new (sub)releases of the Annotations will normally reflect your suggestions for improvements. Except for the incidental case I will not otherwise acknowledge the receipt of suggestions for improvements. Please don't misinterpret this for lack of appreciation.
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.
An example showing the use of the class is:
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++) // produeces boundary overlflow 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 operator[](unsigned index) const; // second private: void boundary(unsigned index) const; void copy(IntArray const &other); };This class has the following characteristics:
int
argument,
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 and as
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 the const
, as we must be able to change the element we want to
access, if 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
, 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
used with const
objects. It can only be used for
value-retrieval, not
for value-assignment, but that is precisely what we want with const
objects.
Furthermore note that, since the values stored in the IntArray
are
primitive valuees of type int
, using
value return types is ok
here. 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. An int const &
could have been
used in the class IntArray
as well. The second overloaded index operator
would than get the following prototype:
int const & IntArray::operator[](int index) const
delete data
. Therefore, our
standard destroy()
function was not used.
data
are int
s, no delete[]
is needed.
#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::operator[](unsigned index) { boundary(index); return d_data[index]; } int IntArray::operator[](unsigned index) const { boundary(index); return d_data[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); } }
cout
and
cerr
and the
insertion operator
operator<<()
. Adapting a class in such a way
that the
istream
's
extraction operator (
operator>>()
) can be used,
occurs in a similar way 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
iostream
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
. After making 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.getName() << "Address: " << pers.getAddress() << "Phone: " << pers.getPhone(); }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 (
operator>>()
). A simple implementation is:
istream &operator>>(istream &str, Person &p) { string name, address, 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 assingment 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 linked 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; unsigned d_n; public: StringArray(unsigned size); StringArray(StringArray const &other); StringArray const &operator=(StringArray const &rvalue); ~StringArray(); String &operator[](unsigned index); };Using the
StringArray::operator []
, assignments between the
String
elements can simply be realized:
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.10). 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 procedure 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.
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 parameter values:
Person(char const *name, char const *address = "<unknwon>", 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 352", "050 363 3688");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 former Person
constructor: the second and
third parameters use their
default values. Here, and
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 according to the new table size.
Such a class could (partially) be defined as follows:
class HashTable { public: HashTable(unsigned n); // n: initial table size unsigned size(); // returns current # of elements // add new key and value void add(string const &key, string const &value); };Now consider the following implementation of
add()
:
void HashTable::add(string const &key, string const &value) { if (size() > d_n * 0.75) // table gets rather full d_ht = 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. This eventually
succeeds, but the cost is way too high: accidentally the programmer assigns an
unsigned value, intending to tell the hashtable what its new size should
be. What happens next comes as a bit of a surprise:
operator=(unsigned newsize)
is
available for HashTable
.
HashTable
that can accomodate 2 * size()
elements.
HashTable
is now 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 char const *
to Person
the class interface of the
class Person
must contain the constructor
explicit Person(char const *name, char const *address = "<unknwon>", char const *phone = "<unknown>");Note, that it is still possible to add `
karel
' to the dbase
object, but it can't be realized through an implicit conversion anymore. To
add `karel
' to the database, the Person
constructor must be explicitly
called, creating an
anonymous object:
dbase += Person("karel");Similarly, the assignment
ht = size() * 2
is now caught by the
compiler, complaining with a message like no match for `HashTable& =
unsigned int' operator.
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, so we have an expression that has 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 unsigned
value
type. The class could have the following (partially shown) interface:
class Unsigned { unsigned d_value; public: Unsigned(); Unsigned(unsigned 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 a consequent application of the notion of overloaded
functions. Whatever your opinion in this matter, we can conclude the
following:
Unsigned
wrapper class,
add the following line to the class interface:
Unsigned &operator++(int);
The implementation of the postfix increment operator should be like this:
Unsigned Unsigned::operator++(int) { return d_value++; }The simplicity of this implementation is deceiving, however. Note that:
value
is used with a postfix increment in the
return
expression. Therefore, the value of the return
expression is value
's
value, before it is incremented; which is correct.
Unsigned
value. This
anonymous object is implicitly initialized by the value of 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, while 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 which initializes 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 a (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 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.8.
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, 0
At 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.
operatornew
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 by the
constructor. 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 // new allocates the memory for an X object *ptr = X::operator new(); X::X(ptr); // now let the constructor operate 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
first 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
to the constructor (as the this
pointer) 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, unsigned 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 *
.
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 returntype 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.8.
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'
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 wil 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.
To overload 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, unsigned 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)]
Alternatively, using
::new char [size]
the required (uninitialized) amount of memory can be allocated too.
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 extra parameters. The second
form of the overloaded operator new[]
shows such an extra unsigned
parameter. The definition of such a function is standard, and could be:
void *Object::operator new[](size_t size, unsigned extra) { unsigned n = size / sizeof(Object); Object *op = ::new Object[n]; for (unsigned idx = 0; idx < n; idx++) op[idx].value = extra; // assume a member `value' return cp; }To use this overloaded operator, only the extra 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
member set to 100.
Like 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
. However, the compiler expects ::delete[]
to receive a
pointer to Objects
, so a
type cast is necessary:
::delete[] reinterpret_cast<Object *>(p)
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 extra 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 shoulld not be defined, to avoid
ambiguity. An example of this form of
operator delete[]
is:
void Object::operator delete[](void *p, unsigned size) { cout << "deleting " << size << " bytes\n"; ::delete [] reinterpret_cast<Object *>(p); }If other 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 behind the brackets:
delete[](new Object[5], 100, false);
operator()()
. By defining the function
call operator an object may be used as a function, hence the term
function objects.
Function objects play an important role in the generic algorithms and they can be used profitably as alternatives to using pointers to functions. The fact that they are important in the context of the generic algorithms constitutes some sort of a didactical dilemma: at this point it would have been nice if the generic algorithms would have been covered, but for the discussion of the generic algorithms knowledge of function objects is an advantage. This bootstrapping problem is solved in a well known way: by ignoring the dependency.
Function objects are objects for which the operator()()
has been
defined. Usually they are used in combination with generic algorithms, but
they are also used 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.
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 in which this function is used is, e.g.,
Person &target = targetPerson(), // determine the person we're looking for *pArray; unsigned 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 the average n / 2
times at least the
following actions take place:
lsearch()
is evaluated, producing the
address of compareFunction()
;
Person::operator!=()
argument is pushed on the stack;
operator!=()
function is evaluated;
Person::operator!=()
argument is popped off the
stack;
PersonSearch()
, having the following prototype
(realize that this 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(), *pArray; unsigned 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:
int 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 not outweight 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 of course access their private data
directly. 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.
Actually, in the above example operator()()
could have been avoided
altogether in the above example. However, function objects play a central role
in generic algorithms. In chapter 17 these generic algorithms are
further discussed. Also in that chapter, the importance of function objects
will be further emphasized when
predefined function objects are
discussed.
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 <iosfwd> #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. The following section introduces a way to implement manipulators requiring arguments without resorting to macros, but using anonymous objects.
9.9.1.1: Manipulators requiring arguments
Manipulators taking arguments are implemented as
macros: they are
handled by the
preprocessor, and are not available beyond the preprocessing
stage. The problem appears to be that you can't call a function in an
insertion sequence: in a sequence of operator<<()
calls the compiler
will call the functions first, 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:
Manip
, whose
constructor expects the argument or multiple arguments we need to use.
ostream &operator<< (ostream & ostr, Manip const &mm)so a
Manip
object can be inserted into the ostream.
Manip
could be, e.g.,
class Manip { int d_value; friend ostream &operator<<(ostream &str, Manip const &x) { return str << x.d_value; } public: Manip(int value) : d_value(value) {} };Now we're getting to where we want to be: by inserting anonymous
Manip
objects into an ostream
the desired effect is reached:
manipulators having (multiple) arguments. E.g.,
int main() { cout << Manip(4) << " -- " << Manip(5) << " ++ " << Manip(6) << endl; } /* generated output: 4 -- 5 ++ 6 */Note that in order to be able to insert an anonymous
MultiMap
object
into the ostream
, the MultiMap
's operator<<()
friend must
define a Manip const &
parameter.
+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= *= /= %= ^= &= |= <<= >>= [] () -> ->* new deleteSeveral 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.