| Chapter 6: More About Operator Overloading |  |  |  |
Now that we've covered the overloaded assignment operator in depth, and
now that we've seen some examples of other overloaded operators as well
(i.e., the insertion and extraction operators), let's take a look at some
other interesting examples of operator overloading.
6.1: Overloading operator[]()
As our next example of operator overloading, we present a class which is
meant to operate on an array of
ints. Indexing the array elements occurs
with the standard array operator
[], but additionally the class checks
for boundary overflow. Furthermore, the array operator is interesting in that
it both
produces a value and
accepts a value, when used, respectively,
as a
right-hand value and a
left-hand value in expressions.
An example of the use of the class is given here:
int main()
{
IntArray
x(20); // 20 ints
for (int i = 0; i < 20; i++)
x[i] = i * 2; // assign the elements
// produces boundary
// overflow
for (int i = 0; i <= 20; i++)
cout << "At index " << i << ": value is " << x[i] << endl;
return (0);
}
This example shows how an array is created to contain 20
ints. The elements
of the array can be assigned or retrieved. The above example should produce a
run-time error, generated by the class
IntArray: the last
for loop causing a boundary overflow, since
x[20] is addressed while
legal indices range from 0 to 19, inclusive.
We give the following class interface:
class IntArray
{
public:
IntArray(int size = 1); // default size: 1 int
IntArray(IntArray const &other);
~IntArray();
IntArray const &operator=(IntArray const &other);
// overloaded index operators:
int &operator[](int index); // first
int operator[](int index) const; // second
private:
void destroy(); // standard functions
// used to copy/destroy
void copy(IntArray const &other);
int
*data,
size;
};
Concerning this class interface we remark:
- The class has a constructor with a default
int argument,
specifying the array size. This function serves also as the default
constructor, since the compiler will substitute 1 for the argument when
none is given.
- The class internally uses a pointer to reach allocated memory.
Hence, the necessary tools are provided: a copy constructor, an overloaded
assignment function and a destructor.
- Note that there are two overloaded index operators. Why are there
two of them ?
The first overloaded index operator allows us to reach and obtain
the elements of the IntArray object.
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]
on the left-hand side and on the right-hand side of an assignment.
We can
therefore use the same function to retrieve and to assign values.
Furthermore note that the returnvalue of the overloaded array operator is
not a int const &, but rather a int &. In this situation we
don't want the const, as we must be able to change the element
we want to access, if the operator is used as a left-hand value in an
assignment.
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 cannot be used with non-const
objects, but it's perfect for const objects. It can only be used for
value-retrieval, not for value-assignment, but that is precisely what we want
with const objects.
- We used the standard implementations of the copy constructor,
the overloaded assignment operator and the destructor, discussed before
(in section 5.4.1), albeit that we've left out the
implementation of the function
destroy(), as this function would
consist of merely one statement (delete data).
- As the elements of
data are ints, no delete [] is needed.
It does no harm, either. Therefore, since we use the [] when the
object is created, we also use the [] when the data are eventually
destroyed.
The member functions of the class are presented next.
IntArray::IntArray(int sz)
{
if (sz < 1)
{
cout << "IntArray: size of array must be >= 1, not " << sz
<< "!" << endl;
exit(1);
}
// remember size, create array
size = sz;
data = new int [sz];
}
// copy constructor
IntArray::IntArray(IntArray const &other)
{
copy(other);
}
// destructor
IntArray::~IntArray()
{
delete [] data;
}
// overloaded assignment
IntArray const &IntArray::operator=(IntArray const &other)
{
// take action only when no auto-assignment
if (this != &other)
{
delete [] data;
copy(other);
}
return (*this);
}
// copy() primitive
void IntArray::copy(IntArray const &other)
{
// set size
size = other.size;
// create array
data = new int [size];
// copy other's values
for (register int i = 0; i < size; i++)
data[i] = other.data[i];
}
// here is the overloaded array operator
int &IntArray::operator[](int index)
{
// check for array boundary over/underflow
if (index < 0 || index >= size)
{
cout << "IntArray: boundary overflow or underflow, index = "
<< index << ", should range from 0 to " << size - 1 << endl;
exit(1);
}
return (data[index]); // emit the reference
}
6.2: Overloading operator new(size_t)
If the
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
stddef.h, 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();
Furthermore, the new [] construction will always use the default operator
new.
An example of the overloaded operator new for the class X is the
following:
#include <stddef.h>
void *X::operator new(size_t sizeofX)
{
void
*p = new char[sizeofX];
return (memset(p, 0, sizeof(X)));
}
Now, what happens when the operator new is defined for the class X,
assuming 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
x,
y,
z;
};
Next, consider the following program fragment:
#include "X.h" // class X interface etc.
int main()
{
X
*x = new X();
cout << x.x << ", " << x.y << ", "<< x.z << endl;
return (0);
}
This small program produces the following output:
0, 0, 0
Our little program performed the following actions:
- First, operator new was called, which allocated and initialized
a block of memory, the size of an
X object.
- Next, a pointer to this block of memory was passed to the
(default)
X() constructor. Since no constructor was defined,
the constructor itself didn't do anything at all.
Due to the initialization of the block of memory by the
new operator
the allocated
X object was already initialized to zeros when the
constructor was called.
Non-static object member functions are passed a (hidden) pointer to the object
on which they should operate. This hidden pointer becomes the this pointer
inside the memberfunction. This procedure is also followed by the constructor.
In the following fragments of pseudo C++
the pointer is made visible. In the first
part an X object is declared 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
// the compiler made 'x' available
void // ask new to allocate the memory for an X
*ptr = X::operator new();
X::X(ptr); // and 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 functions of the class
X. Actually, the
operator new()
operator
is a static functions 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 first. It can do that by allocating enough memory, and
by initializing the area as required. Next, the memory is passed over to the
constructor (as the
this pointer) for further processing. The fact that
an overloaded operator
new is in fact a static function, not requiring
an object of its class can be illustrated in the following (discouraged
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);
return (0);
}
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 again
is the size_t parameter, other parameters must be passed during the
call to the operator new. 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
*object1 = new(12) X(),
*object2 = new("%d %d", 12, 13) X(),
*object3 = new("%d", 12) X();
return (0);
}
The object (object1) 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 object (object2) 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
object3 also uses the second overloaded
operator new():
that overloaded operator accepts a variable number of arguments, the first
of which is a
char const *.
6.3: Overloading operator delete(void *)
The
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);
The `home-made' delete operator is called after executing the class'
destructor. 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 itself.
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)
{
// ... whatever else is considered necessary
// use the default operator delete
::delete ptr;
}
6.4: Cin, cout, cerr and their operators
This section describes how a class can be adapted in such a way that it
can be used with the
C++ streams
cout and
cerr and the insertion operator
<<. Adaptation of a class for the usage with
cin and
its extraction operator
>> occurs in a similar way and is not illustrated
here.
The implementation of an overloaded operator << in the context
of cout or cerr involves the base class of cout or
cerr, which is ostream. This class is declared in the header
file iostream.h and defines only overloaded operator functions for
`basic' types, such as, int, char*, etc.. The purpose of
this section is to show how an operator function can be defined which
processes a new class, say Person (see chapter 5.1) ,
so that constructions as the following one become possible:
Person
kr("Kernighan and Ritchie", "unknown", "unknown");
cout << "Name, address and phone number of Person kr:\n"
<< kr
<< '\n';
The statement cout << kr involves the operator <<
and its two operands: an ostream & and a Person &. The
proposed action is defined in a class-less operator function
operator<<() expecting two arguments:
// declaration in, say, person.h
ostream &operator<<(ostream &, Person const &);
// definition in some source file
ostream &operator<<(ostream &stream, Person const &pers)
{
return
(
stream << "Name: " << pers.getname()
<< "Address: " << pers.getaddress()
<< "Phone: " << pers.getphone()
);
}
Concerning this function we remark the following:
- The function must return a (reference to)
ostream object,
to enable `chaining' of the operator.
- The two operands of the operator
<< are stated as
the two arguments of the overloading function.
- The class
ostream provides the member function
opfx(), which flushes any other ostream streams tied
with the current stream. opfx() returns 0 when an error has been
encountered (Cf. chapter 9).
An improved form of the above function would therefore be:
ostream &operator<<(ostream &stream, Person const &pers)
{
if (! stream.opfx())
return (stream);
...
}
6.5: Conversion operators
A class may be constructed around a basic type. E.g., it is often fruitful
to define a class
String around the
char *. Such a class may define all
kinds of operations, like assignments. Take a look at the following
class interface:
class String
{
public:
String();
String(char const *arg);
~String();
String(String const &other);
String const &operator=(String const &rvalue);
String const &operator=(char const *rvalue);
private:
char
*string;
};
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::getstr() const. However, in the current context that
looks a bit awkward, but it also doesn't seem to be the right way to
go when an array of strings is defined, e.g., in a class StringArray,
in which the operator[] is implemented to allow the access of individual
strings. Take a look at the following class interface:
class StringArray
{
public:
StringArray(unsigned size);
StringArray(StringArray const &other);
StringArray const &operator=(StringArray const &rvalue);
~StringArray();
String &operator[](unsigned index);
private:
String
*store;
unsigned
n;
};
The StringArray class has one interesting memberfunction: the overloaded
array operator operator[]. It returns a String reference.
Using this operator assignments between the String elements can be
realized:
StringArray
sa(10);
... // assume the array is filled here
sa[4] = sa[3]; // String to String assignment
It is also possible to assign a char const * to an element of sa:
sa[3] = "hello world";
When this is evaluated, the following steps are followed:
- First,
sa[3] is evaluated. This results in a String reference.
- Next, the
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.
Now we try to do it the other way around: how to access the
char const * that's stored in sa[3]? We try the following code:
char const
*cp;
cp = sa[3];
Well, this won't work: we would need an overloaded assignment operator for the
'class char const *'. However, there isn't such a class, and therefore we
can't build that overloaded assignment operator (see also section
6.6). Furthermore,
casting won't work: the
compiler doesn't know how to cast a
String to a
char const *.
How to proceed?
The naive solution is to resort to the accessor member function getstr():
cp = sa[3].getstr();
That solution would work, but it looks so clumsy.... A far better approach
would be to 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 *. The general form of a conversion operator in the class
interface is:
operator <type>();
With our
String class, it would therefore be:
operator char const *();
The implementation of the conversion operator is straightforward:
String::operator char const *()
{
return (string);
}
Notes:
- There is no mentioning of a return type. The conversion operator
has the type of the returned value just after the
operator keyword.
- In certain situations the compiler needs a hand to disambiguate our
intentions. In a statement like
printf("%s", sa[3]);
the compiler is confused: are we going to pass a String & or a
char const * to the printf() function? To help the compiler
out, we supply an explicit cast here:
printf("%s", (char const *)sa[3]);
For completion, the final String class interface, containing the
conversion operator, looks like this:
class String
{
public:
String();
String(char const *arg);
~String();
String(String const &other);
String const &operator=(String const &rvalue);
String const &operator=(char const *rvalue);
operator char const *();
private:
char
*string;
};
6.6: Overloadable Operators
The following operators can be overloaded:
+ - * / % ^ & |
~ ! , = < > <= >=
++ -- << >> == != && ||
+= -= *= /= %= ^= &= |=
<<= >>= [] () -> ->* new delete
However, some 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, as we have seen in section
6.5.