ZeePedia

Operator Overloading:Overloadable operators, Overloading assignment

<< References & the Copy-Constructor:The copyconstructor, Pointers to members
Dynamic Object Creation:Early examples redesigned, new & delete for arrays >>
img
12: Operator Overloading
Operator overloading is just "syntactic sugar," which
means it is simply another way for you to make a
function call.
511
img
The difference is that the arguments for this function don't appear
inside parentheses, but instead they surround or are next to
characters you've always thought of as immutable operators.
There are two differences between the use of an operator and an
ordinary function call. The syntax is different; an operator is often
"called" by placing it between or sometimes after the arguments.
The second difference is that the compiler determines which
"function" to call. For instance, if you are using the operator + with
floating-point arguments, the compiler "calls" the function to
perform floating-point addition (this "call" is typically the act of
inserting in-line code, or a floating-point-processor instruction). If
you use operator + with a floating-point number and an integer,
the compiler "calls" a special function to turn the int into a float,
and then "calls" the floating-point addition code.
But in C++, it's possible to define new operators that work with
classes. This definition is just like an ordinary function definition
except that the name of the function consists of the keyword
operatorfollowed by the operator. That's the only difference, and it
becomes a function like any other function, which the compiler
calls when it sees the appropriate pattern.
Warning & reassurance
It's tempting to become overenthusiastic with operator
overloading. It's a fun toy, at first. But remember it's only syntactic
sugar, another way of calling a function. Looking at it this way, you
have no reason to overload an operator except if it will make the
code involving your class easier to write and especially easier to
read. (Remember, code is read much more than it is written.) If this
isn't the case, don't bother.
Another common response to operator overloading is panic;
suddenly, C operators have no familiar meaning anymore.
"Everything's changed and all my C code will do different things!"
512
Thinking in C++
img
This isn't true. All the operators used in expressions that contain
only built-in data types cannot be changed. You can never overload
operators such that
1 << 4;
behaves differently, or
1.414 << 2;
has meaning. Only an expression containing a user-defined type
can have an overloaded operator.
Syntax
Defining an overloaded operator is like defining a function, but the
name of that function is operator@ in which @ represents the
,
operator that's being overloaded. The number of arguments in the
overloaded operator's argument list depends on two factors:
1.
Whether it's a unary operator (one argument) or a binary
operator (two arguments).
2.
Whether the operator is defined as a global function (one
argument for unary, two for binary) or a member function
(zero arguments for unary, one for binary ­ the object
becomes the left-hand argument).
Here's a small class that shows the syntax for operator overloading:
//: C12:OperatorOverloadingSyntax.cpp
#include <iostream>
using namespace std;
class Integer {
int i;
public:
Integer(int ii) : i(ii) {}
const Integer
operator+(const Integer& rv) const {
12: Operator Overloading
513
img
cout << "operator+" << endl;
return Integer(i + rv.i);
}
Integer&
operator+=(const Integer& rv) {
cout << "operator+=" << endl;
i += rv.i;
return *this;
}
};
int main() {
cout << "built-in types:" << endl;
int i = 1, j = 2, k = 3;
k += i + j;
cout << "user-defined types:" << endl;
Integer ii(1), jj(2), kk(3);
kk += ii + jj;
} ///:~
The two overloaded operators are defined as inline member
functions that announce when they are called. The single argument
is what appears on the right-hand side of the operator for binary
operators. Unary operators have no arguments when defined as
member functions. The member function is called for the object on
the left-hand side of the operator.
For non-conditional operators (conditionals usually return a
Boolean value), you'll almost always want to return an object or
reference of the same type you're operating on if the two
arguments are the same type. (If they're not the same type, the
interpretation of what it should produce is up to you.) This way,
complicated expressions can be built up:
kk += ii + jj;
The operator+produces a new Integer (a temporary) that is used as
the rv argument for the operator+= This temporary is destroyed as
.
soon as it is no longer needed.
514
Thinking in C++
img
Overloadable operators
Although you can overload almost all the operators available in C,
the use of operator overloading is fairly restrictive. In particular,
you cannot combine operators that currently have no meaning in C
(such as ** to represent exponentiation), you cannot change the
evaluation precedence of operators, and you cannot change the
number of arguments required by an operator. This makes sense ­
all of these actions would produce operators that confuse meaning
rather than clarify it.
The next two subsections give examples of all the "regular"
operators, overloaded in the form that you'll most likely use.
Unary operators
The following example shows the syntax to overload all the unary
operators, in the form of both global functions (non-member friend
functions) and as member functions. These will expand upon the
Integer class shown previously and add a new byte class. The
meaning of your particular operators will depend on the way you
want to use them, but consider the client programmer before doing
something unexpected.
Here is a catalog of all the unary functions:
//: C12:OverloadingUnaryOperators.cpp
#include <iostream>
using namespace std;
// Non-member functions:
class Integer {
long i;
Integer* This() { return this; }
public:
Integer(long ll = 0) : i(ll) {}
// No side effects takes const& argument:
friend const Integer&
operator+(const Integer& a);
friend const Integer
12: Operator Overloading
515
img
operator-(const Integer& a);
friend const Integer
operator~(const Integer& a);
friend Integer*
operator&(Integer& a);
friend int
operator!(const Integer& a);
// Side effects have non-const& argument:
// Prefix:
friend const Integer&
operator++(Integer& a);
// Postfix:
friend const Integer
operator++(Integer& a, int);
// Prefix:
friend const Integer&
operator--(Integer& a);
// Postfix:
friend const Integer
operator--(Integer& a, int);
};
// Global operators:
const Integer& operator+(const Integer& a) {
cout << "+Integer\n";
return a; // Unary + has no effect
}
const Integer operator-(const Integer& a) {
cout << "-Integer\n";
return Integer(-a.i);
}
const Integer operator~(const Integer& a) {
cout << "~Integer\n";
return Integer(~a.i);
}
Integer* operator&(Integer& a) {
cout << "&Integer\n";
return a.This(); // &a is recursive!
}
int operator!(const Integer& a) {
cout << "!Integer\n";
return !a.i;
}
// Prefix; return incremented value
const Integer& operator++(Integer& a) {
516
Thinking in C++
img
cout << "++Integer\n";
a.i++;
return a;
}
// Postfix; return the value before increment:
const Integer operator++(Integer& a, int) {
cout << "Integer++\n";
Integer before(a.i);
a.i++;
return before;
}
// Prefix; return decremented value
const Integer& operator--(Integer& a) {
cout << "--Integer\n";
a.i--;
return a;
}
// Postfix; return the value before decrement:
const Integer operator--(Integer& a, int) {
cout << "Integer--\n";
Integer before(a.i);
a.i--;
return before;
}
// Show that the overloaded operators work:
void f(Integer a) {
+a;
-a;
~a;
Integer* ip = &a;
!a;
++a;
a++;
--a;
a--;
}
// Member functions (implicit "this"):
class Byte {
unsigned char b;
public:
Byte(unsigned char bb = 0) : b(bb) {}
// No side effects: const member function:
const Byte& operator+() const {
12: Operator Overloading
517
img
cout << "+Byte\n";
return *this;
}
const Byte operator-() const {
cout << "-Byte\n";
return Byte(-b);
}
const Byte operator~() const {
cout << "~Byte\n";
return Byte(~b);
}
Byte operator!() const {
cout << "!Byte\n";
return Byte(!b);
}
Byte* operator&() {
cout << "&Byte\n";
return this;
}
// Side effects: non-const member function:
const Byte& operator++() { // Prefix
cout << "++Byte\n";
b++;
return *this;
}
const Byte operator++(int) { // Postfix
cout << "Byte++\n";
Byte before(b);
b++;
return before;
}
const Byte& operator--() { // Prefix
cout << "--Byte\n";
--b;
return *this;
}
const Byte operator--(int) { // Postfix
cout << "Byte--\n";
Byte before(b);
--b;
return before;
}
};
void g(Byte b) {
518
Thinking in C++
img
+b;
-b;
~b;
Byte* bp = &b;
!b;
++b;
b++;
--b;
b--;
}
int main() {
Integer a;
f(a);
Byte b;
g(b);
} ///:~
The functions are grouped according to the way their arguments
are passed. Guidelines for how to pass and return arguments are
given later. The forms above (and the ones that follow in the next
section) are typically what you'll use, so start with them as a
pattern when overloading your own operators.
Increment & decrement
The overloaded ++ and ­ ­ operators present a dilemma because
you want to be able to call different functions depending on
whether they appear before (prefix) or after (postfix) the object
they're acting upon. The solution is simple, but people sometimes
find it a bit confusing at first. When the compiler sees, for example,
++a (a pre-increment), it generates a call to operator++(a) but
;
when it sees a++, it generates a call to operator++(a, int)That is,
.
the compiler differentiates between the two forms by making calls
to different overloaded functions. In
OverloadingUnaryOperators.cpp the member function
for
versions, if the compiler sees ++b, it generates a call to
B::operator++( ;)if it sees b++ it calls B::operator++(int)
.
All the user sees is that a different function gets called for the prefix
and postfix versions. Underneath, however, the two functions calls
12: Operator Overloading
519
img
have different signatures, so they link to two different function
bodies. The compiler passes a dummy constant value for the int
argument (which is never given an identifier because the value is
never used) to generate the different signature for the postfix
version.
Binary operators
The following listing repeats the example of
OverloadingUnaryOperators.cpp binary operators so you have
for
an example of all the operators you might want to overload. Again,
both global versions and member function versions are shown.
//: C12:Integer.h
// Non-member overloaded operators
#ifndef INTEGER_H
#define INTEGER_H
#include <iostream>
// Non-member functions:
class Integer {
long i;
public:
Integer(long ll = 0) : i(ll) {}
// Operators that create new, modified value:
friend const Integer
operator+(const Integer& left,
const Integer& right);
friend const Integer
operator-(const Integer& left,
const Integer& right);
friend const Integer
operator*(const Integer& left,
const Integer& right);
friend const Integer
operator/(const Integer& left,
const Integer& right);
friend const Integer
operator%(const Integer& left,
const Integer& right);
friend const Integer
operator^(const Integer& left,
520
Thinking in C++
img
const Integer& right);
friend const Integer
operator&(const Integer& left,
const Integer& right);
friend const Integer
operator|(const Integer& left,
const Integer& right);
friend const Integer
operator<<(const Integer& left,
const Integer& right);
friend const Integer
operator>>(const Integer& left,
const Integer& right);
// Assignments modify & return lvalue:
friend Integer&
operator+=(Integer& left,
const Integer& right);
friend Integer&
operator-=(Integer& left,
const Integer& right);
friend Integer&
operator*=(Integer& left,
const Integer& right);
friend Integer&
operator/=(Integer& left,
const Integer& right);
friend Integer&
operator%=(Integer& left,
const Integer& right);
friend Integer&
operator^=(Integer& left,
const Integer& right);
friend Integer&
operator&=(Integer& left,
const Integer& right);
friend Integer&
operator|=(Integer& left,
const Integer& right);
friend Integer&
operator>>=(Integer& left,
const Integer& right);
friend Integer&
operator<<=(Integer& left,
const Integer& right);
// Conditional operators return true/false:
12: Operator Overloading
521
img
friend int
operator==(const Integer& left,
const Integer& right);
friend int
operator!=(const Integer& left,
const Integer& right);
friend int
operator<(const Integer& left,
const Integer& right);
friend int
operator>(const Integer& left,
const Integer& right);
friend int
operator<=(const Integer& left,
const Integer& right);
friend int
operator>=(const Integer& left,
const Integer& right);
friend int
operator&&(const Integer& left,
const Integer& right);
friend int
operator||(const Integer& left,
const Integer& right);
// Write the contents to an ostream:
void print(std::ostream& os) const { os << i; }
};
#endif // INTEGER_H ///:~
//: C12:Integer.cpp {O}
// Implementation of overloaded operators
#include "Integer.h"
#include "../require.h"
const Integer
operator+(const Integer& left,
const Integer& right) {
return Integer(left.i + right.i);
}
const Integer
operator-(const Integer& left,
const Integer& right) {
return Integer(left.i - right.i);
}
const Integer
522
Thinking in C++
img
operator*(const Integer& left,
const Integer& right) {
return Integer(left.i * right.i);
}
const Integer
operator/(const Integer& left,
const Integer& right) {
require(right.i != 0, "divide by zero");
return Integer(left.i / right.i);
}
const Integer
operator%(const Integer& left,
const Integer& right) {
require(right.i != 0, "modulo by zero");
return Integer(left.i % right.i);
}
const Integer
operator^(const Integer& left,
const Integer& right) {
return Integer(left.i ^ right.i);
}
const Integer
operator&(const Integer& left,
const Integer& right) {
return Integer(left.i & right.i);
}
const Integer
operator|(const Integer& left,
const Integer& right) {
return Integer(left.i | right.i);
}
const Integer
operator<<(const Integer& left,
const Integer& right) {
return Integer(left.i << right.i);
}
const Integer
operator>>(const Integer& left,
const Integer& right) {
return Integer(left.i >> right.i);
}
// Assignments modify & return lvalue:
Integer& operator+=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
12: Operator Overloading
523
img
left.i += right.i;
return left;
}
Integer& operator-=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i -= right.i;
return left;
}
Integer& operator*=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i *= right.i;
return left;
}
Integer& operator/=(Integer& left,
const Integer& right) {
require(right.i != 0, "divide by zero");
if(&left == &right) {/* self-assignment */}
left.i /= right.i;
return left;
}
Integer& operator%=(Integer& left,
const Integer& right) {
require(right.i != 0, "modulo by zero");
if(&left == &right) {/* self-assignment */}
left.i %= right.i;
return left;
}
Integer& operator^=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i ^= right.i;
return left;
}
Integer& operator&=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i &= right.i;
return left;
}
Integer& operator|=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i |= right.i;
524
Thinking in C++
img
return left;
}
Integer& operator>>=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i >>= right.i;
return left;
}
Integer& operator<<=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i <<= right.i;
return left;
}
// Conditional operators return true/false:
int operator==(const Integer& left,
const Integer& right) {
return left.i == right.i;
}
int operator!=(const Integer& left,
const Integer& right) {
return left.i != right.i;
}
int operator<(const Integer& left,
const Integer& right) {
return left.i < right.i;
}
int operator>(const Integer& left,
const Integer& right) {
return left.i > right.i;
}
int operator<=(const Integer& left,
const Integer& right) {
return left.i <= right.i;
}
int operator>=(const Integer& left,
const Integer& right) {
return left.i >= right.i;
}
int operator&&(const Integer& left,
const Integer& right) {
return left.i && right.i;
}
int operator||(const Integer& left,
const Integer& right) {
12: Operator Overloading
525
img
return left.i || right.i;
} ///:~
//: C12:IntegerTest.cpp
//{L} Integer
#include "Integer.h"
#include <fstream>
using namespace std;
ofstream out("IntegerTest.out");
void h(Integer& c1, Integer& c2) {
// A complex expression:
c1 += c1 * c2 + c2 % c1;
#define TRY(OP) \
out << "c1 = "; c1.print(out); \
out << ", c2 = "; c2.print(out); \
out << ";  c1 " #OP " c2 produces "; \
(c1 OP c2).print(out); \
out << endl;
TRY(+) TRY(-) TRY(*) TRY(/)
TRY(%) TRY(^) TRY(&) TRY(|)
TRY(<<) TRY(>>) TRY(+=) TRY(-=)
TRY(*=) TRY(/=) TRY(%=) TRY(^=)
TRY(&=) TRY(|=) TRY(>>=) TRY(<<=)
// Conditionals:
#define TRYC(OP) \
out << "c1 = "; c1.print(out); \
out << ", c2 = "; c2.print(out); \
out << ";  c1 " #OP " c2 produces "; \
out << (c1 OP c2); \
out << endl;
TRYC(<) TRYC(>) TRYC(==) TRYC(!=) TRYC(<=)
TRYC(>=) TRYC(&&) TRYC(||)
}
int main() {
cout << "friend functions" << endl;
Integer c1(47), c2(9);
h(c1, c2);
} ///:~
//: C12:Byte.h
// Member overloaded operators
#ifndef BYTE_H
#define BYTE_H
526
Thinking in C++
img
#include "../require.h"
#include <iostream>
// Member functions (implicit "this"):
class Byte {
unsigned char b;
public:
Byte(unsigned char bb = 0) : b(bb) {}
// No side effects: const member function:
const Byte
operator+(const Byte& right) const {
return Byte(b + right.b);
}
const Byte
operator-(const Byte& right) const {
return Byte(b - right.b);
}
const Byte
operator*(const Byte& right) const {
return Byte(b * right.b);
}
const Byte
operator/(const Byte& right) const {
require(right.b != 0, "divide by zero");
return Byte(b / right.b);
}
const Byte
operator%(const Byte& right) const {
require(right.b != 0, "modulo by zero");
return Byte(b % right.b);
}
const Byte
operator^(const Byte& right) const {
return Byte(b ^ right.b);
}
const Byte
operator&(const Byte& right) const {
return Byte(b & right.b);
}
const Byte
operator|(const Byte& right) const {
return Byte(b | right.b);
}
const Byte
operator<<(const Byte& right) const {
return Byte(b << right.b);
12: Operator Overloading
527
img
}
const Byte
operator>>(const Byte& right) const {
return Byte(b >> right.b);
}
// Assignments modify & return lvalue.
// operator= can only be a member function:
Byte& operator=(const Byte& right) {
// Handle self-assignment:
if(this == &right) return *this;
b = right.b;
return *this;
}
Byte& operator+=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b += right.b;
return *this;
}
Byte& operator-=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b -= right.b;
return *this;
}
Byte& operator*=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b *= right.b;
return *this;
}
Byte& operator/=(const Byte& right) {
require(right.b != 0, "divide by zero");
if(this == &right) {/* self-assignment */}
b /= right.b;
return *this;
}
Byte& operator%=(const Byte& right) {
require(right.b != 0, "modulo by zero");
if(this == &right) {/* self-assignment */}
b %= right.b;
return *this;
}
Byte& operator^=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b ^= right.b;
return *this;
}
528
Thinking in C++
img
Byte& operator&=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b &= right.b;
return *this;
}
Byte& operator|=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b |= right.b;
return *this;
}
Byte& operator>>=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b >>= right.b;
return *this;
}
Byte& operator<<=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b <<= right.b;
return *this;
}
// Conditional operators return true/false:
int operator==(const Byte& right) const {
return b == right.b;
}
int operator!=(const Byte& right) const {
return b != right.b;
}
int operator<(const Byte& right) const {
return b < right.b;
}
int operator>(const Byte& right) const {
return b > right.b;
}
int operator<=(const Byte& right) const {
return b <= right.b;
}
int operator>=(const Byte& right) const {
return b >= right.b;
}
int operator&&(const Byte& right) const {
return b && right.b;
}
int operator||(const Byte& right) const {
return b || right.b;
}
12: Operator Overloading
529
img
// Write the contents to an ostream:
void print(std::ostream& os) const {
os << "0x" << std::hex << int(b) << std::dec;
}
};
#endif // BYTE_H ///:~
//: C12:ByteTest.cpp
#include "Byte.h"
#include <fstream>
using namespace std;
ofstream out("ByteTest.out");
void k(Byte& b1, Byte& b2) {
b1 = b1 * b2 + b2 % b1;
#define TRY2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << ";  b1 " #OP " b2 produces "; \
(b1 OP b2).print(out); \
out << endl;
b1 = 9; b2 = 47;
TRY2(+) TRY2(-) TRY2(*) TRY2(/)
TRY2(%) TRY2(^) TRY2(&) TRY2(|)
TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
TRY2(=) // Assignment operator
// Conditionals:
#define TRYC2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << ";  b1 " #OP " b2 produces "; \
out << (b1 OP b2); \
out << endl;
b1 = 9; b2 = 47;
TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
TRYC2(>=) TRYC2(&&) TRYC2(||)
// Chained assignment:
Byte b3 = 92;
530
Thinking in C++
img
b1 = b2 = b3;
}
int main() {
out << "member functions:" << endl;
Byte b1(47), b2(9);
k(b1, b2);
} ///:~
You can see that operator=is only allowed to be a member
function. This is explained later.
Notice that all of the assignment operators have code to check for
self-assignment; this is a general guideline. In some cases this is not
necessary; for example, with operator+=you often want to say
A+=A and have it add A to itself. The most important place to
check for self-assignment is operator=because with complicated
objects disastrous results may occur. (In some cases it's OK, but you
should always keep it in mind when writing operator=
.)
All of the operators shown in the previous two examples are
overloaded to handle a single type. It's also possible to overload
operators to handle mixed types, so you can add apples to oranges,
for example. Before you start on an exhaustive overloading of
operators, however, you should look at the section on automatic
type conversion later in this chapter. Often, a type conversion in the
right place can save you a lot of overloaded operators.
Arguments & return values
It may seem a little confusing at first when you look at
OverloadingUnaryOperators.cpp
, Integer.hand Byte.h and see all
the different ways that arguments are passed and returned.
Although you can pass and return arguments any way you want to,
the choices in these examples were not selected at random. They
follow a logical pattern, the same one you'll want to use in most of
your choices.
12: Operator Overloading
531
img
1.
As with any function argument, if you only need to read
from the argument and not change it, default to passing it as
a const reference. Ordinary arithmetic operations (like + and
­, etc.) and Booleans will not change their arguments, so pass
by const reference is predominantly what you'll use. When
the function is a class member, this translates to making it a
const member function. Only with the operator-assignments
(like +=) and the operator= which change the left-hand
,
argument, is the left argument not a constant, but it's still
passed in as an address because it will be changed.
2.
The type of return value you should select depends on the
expected meaning of the operator. (Again, you can do
anything you want with the arguments and return values.) If
the effect of the operator is to produce a new value, you will
need to generate a new object as the return value. For
example, Integer::operator+
must produce an Integer object
that is the sum of the operands. This object is returned by
value as a const, so the result cannot be modified as an
lvalue.
3.
All the assignment operators modify the lvalue. To allow the
result of the assignment to be used in chained expressions,
like a=b=c, it's expected that you will return a reference to
that same lvalue that was just modified. But should this
reference be a const or nonconst? Although you read a=b=c
from left to right, the compiler parses it from right to left, so
you're not forced to return a nonconst to support assignment
chaining. However, people do sometimes expect to be able to
perform an operation on the thing that was just assigned to,
such as (a=b).func( );to call func( ) on a after assigning b to
it. Thus, the return value for all of the assignment operators
should be a nonconst reference to the lvalue.
4.
For the logical operators, everyone expects to get at worst an
int back, and at best a bool. (Libraries developed before most
532
Thinking in C++
img
compilers supported C++'s built-in bool will use int or an
equivalent typedef.)
The increment and decrement operators present a dilemma because
of the pre- and postfix versions. Both versions change the object
and so cannot treat the object as a const. The prefix version returns
the value of the object after it was changed, so you expect to get
back the object that was changed. Thus, with prefix you can just
return *this as a reference. The postfix version is supposed to
return the value before the value is changed, so you're forced to
create a separate object to represent that value and return it. So
with postfix you must return by value if you want to preserve the
expected meaning. (Note that you'll sometimes find the increment
and decrement operators returning an int or bool to indicate, for
example, whether an object designed to move through a list is at
the end of that list.) Now the question is: Should these be returned
as const or nonconst? If you allow the object to be modified and
someone writes (++a).func( , func( ) will be operating on a itself,
)
but with (a++).func( , func( ) operates on the temporary object
)
returned by the postfix operator++ Temporary objects are
.
automatically const, so this would be flagged by the compiler, but
for consistency's sake it may make more sense to make them both
const, as was done here. Or you may choose to make the prefix
version non-const and the postfix const. Because of the variety of
meanings you may want to give the increment and decrement
operators, they will need to be considered on a case-by-case basis.
Return by value as const
Returning by value as a const can seem a bit subtle at first, so it
deserves a bit more explanation. Consider the binary operator+ If
.
you use it in an expression such as f(a+b), the result of a+b
becomes a temporary object that is used in the call to f( ). Because
it's a temporary, it's automatically const, so whether you explicitly
make the return value const or not has no effect.
However, it's also possible for you to send a message to the return
value of a+b, rather than just passing it to a function. For example,
12: Operator Overloading
533
img
you can say (a+b).g( ) in which g( ) is some member function of
,
Integer, in this case. By making the return value const, you state
that only a const member function can be called for that return
value. This is const-correct, because it prevents you from storing
potentially valuable information in an object that will most likely
be lost.
The return optimization
When new objects are created to return by value, notice the form
used. In operator+ for example:
,
return Integer(left.i + right.i);
This may look at first like a "function call to a constructor," but it's
not. The syntax is that of a temporary object; the statement says
"make a temporary Integer object and return it." Because of this,
you might think that the result is the same as creating a named
local object and returning that. However, it's quite different. If you
were to say instead:
Integer tmp(left.i + right.i);
return tmp;
three things will happen. First, the tmp object is created including
its constructor call. Second, the copy-constructor copies the tmp to
the location of the outside return value. Third, the destructor is
called for tmp at the end of the scope.
In contrast, the "returning a temporary" approach works quite
differently. When the compiler sees you do this, it knows that you
have no other need for the object it's creating than to return it. The
compiler takes advantage of this by building the object directly into
the location of the outside return value. This requires only a single
ordinary constructor call (no copy-constructor is necessary) and
there's no destructor call because you never actually create a local
object. Thus, while it doesn't cost anything but programmer
awareness, it's significantly more efficient. This is often called the
return value optimization.
534
Thinking in C++
img
Unusual operators
Several additional operators have a slightly different syntax for
overloading.
The subscript, operator[ ] must be a member function and it
,
requires a single argument. Because operator[ ]implies that the
object it's being called for acts like an array, you will often return a
reference from this operator, so it can be conveniently used on the
left-hand side of an equal sign. This operator is commonly
overloaded; you'll see examples in the rest of the book.
The operators new and delete control dynamic storage allocation
and can be overloaded in a number of different ways. This topic is
covered in the Chapter 13.
Operator comma
The comma operator is called when it appears next to an object of
the type the comma is defined for. However, "operator, is not
"
called for function argument lists, only for objects that are out in
the open, separated by commas. There doesn't seem to be a lot of
practical uses for this operator; it's in the language for consistency.
Here's an example showing how the comma function can be called
when the comma appears before an object, as well as after:
//: C12:OverloadingOperatorComma.cpp
#include <iostream>
using namespace std;
class After {
public:
const After& operator,(const After&) const {
cout << "After::operator,()" << endl;
return *this;
}
};
class Before {};
Before& operator,(int, Before& b) {
12: Operator Overloading
535
img
cout << "Before::operator,()" << endl;
return b;
}
int main() {
After a, b;
a, b;  // Operator comma called
Before c;
1, c;  // Operator comma called
} ///:~
The global function allows the comma to be placed before the object
in question. The usage shown is fairly obscure and questionable.
Although you would probably use a comma-separated list as part
of a more complex expression, it's too subtle to use in most
situations.
Operator->
The operator­>is generally used when you want to make an object
appear to be a pointer. Since such an object has more "smarts" built
into it than exist for a typical pointer, an object like this is often
called a smart pointer. These are especially useful if you want to
"wrap" a class around a pointer to make that pointer safe, or in the
common usage of an iterator, which is an object that moves through
a collection /container of other objects and selects them one at a time,
without providing direct access to the implementation of the
container. (You'll often find containers and iterators in class
libraries, such as in the Standard C++ Library, described in Volume
2 of this book.)
A pointer dereference operator must be a member function. It has
additional, atypical constraints: It must return an object (or
reference to an object) that also has a pointer dereference operator,
or it must return a pointer that can be used to select what the
pointer dereference operator arrow is pointing at. Here's a simple
example:
//: C12:SmartPointer.cpp
536
Thinking in C++
img
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
class Obj {
static int i, j;
public:
void f() const { cout << i++ << endl; }
void g() const { cout << j++ << endl; }
};
// Static member definitions:
int Obj::i = 47;
int Obj::j = 11;
// Container:
class ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
friend class SmartPointer;
};
class SmartPointer {
ObjContainer& oc;
int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++(); // Use prefix version
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned by SmartPointer::operator->()");
return oc.a[index];
}
12: Operator Overloading
537
img
};
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
SmartPointer sp(oc); // Create an iterator
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(sp++);
} ///:~
The class Obj defines the objects that are manipulated in this
program. The functions f( ) and g( ) simply print out interesting
values using static data members. Pointers to these objects are
stored inside containers of type ObjContainerusing its add( )
function. ObjContainerlooks like an array of pointers, but you'll
notice there's no way to get the pointers back out again. However,
SmartPointeris declared as a friend class, so it has permission to
look inside the container. The SmartPointerclass looks very much
like an intelligent pointer ­ you can move it forward using
operator++(you can also define an operator­ ­ it won't go past
),
the end of the container it's pointing to, and it produces (via the
pointer dereference operator) the value it's pointing to. Notice that
the SmartPointeris a custom fit for the container it's created for;
unlike an ordinary pointer, there isn't a "general purpose" smart
pointer. You will learn more about the smart pointers called
"iterators" in the last chapter of this book and in Volume 2
(downloadable from ).
In main( ), once the container oc is filled with Obj objects, a
SmartPointer spis created. The smart pointer calls happen in the
expressions:
sp->f(); // Smart pointer calls
sp->g();
538
Thinking in C++
img
Here, even though sp doesn't actually have f( ) and g( ) member
functions, the pointer dereference operator automatically calls
those functions for the Obj* that is returned by
SmartPointer::operator­>The compiler performs all the checking
.
to make sure the function call works properly.
Although the underlying mechanics of the pointer dereference
operator are more complex than the other operators, the goal is
exactly the same: to provide a more convenient syntax for the users
of your classes.
A nested iterator
It's more common to see a "smart pointer" or "iterator" class nested
within the class that it services. The previous example can be
rewritten to nest SmartPointerinside ObjContainerlike this:
//: C12:NestedSmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
class Obj {
static int i, j;
public:
void f() { cout << i++ << endl; }
void g() { cout << j++ << endl; }
};
// Static member definitions:
int Obj::i = 47;
int Obj::j = 11;
// Container:
class ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
class SmartPointer;
friend SmartPointer;
class SmartPointer {
12: Operator Overloading
539
img
ObjContainer& oc;
unsigned int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++(); // Use prefix version
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned by SmartPointer::operator->()");
return oc.a[index];
}
};
// Function to produce a smart pointer that
// points to the beginning of the ObjContainer:
SmartPointer begin() {
return SmartPointer(*this);
}
};
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
ObjContainer::SmartPointer sp = oc.begin();
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(++sp);
} ///:~
Besides the actual nesting of the class, there are only two
differences here. The first is in the declaration of the class so that it
can be a friend:
540
Thinking in C++
img
class SmartPointer;
friend SmartPointer;
The compiler must first know that the class exists before it can be
told that it's a friend.
The second difference is in the ObjContainermember function
begin( ) which produces a SmartPointerthat points to the
,
beginning of the ObjContainersequence. Although it's really only
a convenience, it's valuable because it follows part of the form used
in the Standard C++ Library.
Operator->*
The operator­>*is a binary operator that behaves like all the other
binary operators. It is provided for those situations when you want
to mimic the behavior provided by the built-in pointer-to-member
syntax, described in the previous chapter.
Just like operator-> the pointer-to-member dereference operator is
,
generally used with some kind of object that represents a "smart
pointer," although the example shown here will be simpler so it's
understandable. The trick when defining operator->*is that it must
return an object for which the operator( )can be called with the
arguments for the member function you're calling.
The function call operator( ) must be a member function, and it is
unique in that it allows any number of arguments. It makes your
object look like it's actually a function. Although you could define
several overloaded operator( )functions with different arguments,
it's often used for types that only have a single operation, or at least
an especially prominent one. You'll see in Volume 2 that the
Standard C++ Library uses the function call operator in order to
create "function objects."
To create an operator->*you must first create a class with an
operator( )that is the type of object that operator->*will return.
This class must somehow capture the necessary information so that
when the operator( )is called (which happens automatically), the
12: Operator Overloading
541
img
pointer-to-member will be dereferenced for the object. In the
following example, the FunctionObjectconstructor captures and
stores both the pointer to the object and the pointer to the member
function, and then the operator( )uses those to make the actual
pointer-to-member call:
//: C12:PointerToMemberOperator.cpp
#include <iostream>
using namespace std;
class Dog {
public:
int run(int i) const {
cout << "run\n";
return i;
}
int eat(int i) const {
cout << "eat\n";
return i;
}
int sleep(int i) const {
cout << "ZZZ\n";
return i;
}
typedef int (Dog::*PMF)(int) const;
// operator->* must return an object
// that has an operator():
class FunctionObject {
Dog* ptr;
PMF pmem;
public:
// Save the object pointer and member pointer
FunctionObject(Dog* wp, PMF pmf)
: ptr(wp), pmem(pmf) {
cout << "FunctionObject constructor\n";
}
// Make the call using the object pointer
// and member pointer
int operator()(int i) const {
cout << "FunctionObject::operator()\n";
return (ptr->*pmem)(i); // Make the call
}
};
542
Thinking in C++
img
FunctionObject operator->*(PMF pmf) {
cout << "operator->*" << endl;
return FunctionObject(this, pmf);
}
};
int main() {
Dog w;
Dog::PMF pmf = &Dog::run;
cout << (w->*pmf)(1) << endl;
pmf = &Dog::sleep;
cout << (w->*pmf)(2) << endl;
pmf = &Dog::eat;
cout << (w->*pmf)(3) << endl;
} ///:~
Dog has three member functions, all of which take an int argument
and return an int. PMF is a typedef to simplify defining a pointer-
to-member to Dog's member functions.
A FunctionObjectis created and returned by operator->* Notice
.
that operator->*knows both the object that the pointer-to-member
is being called for (this) and the pointer-to-member, and it passes
those to the FunctionObjectconstructor that stores the values.
When operator->*is called, the compiler immediately turns around
and calls operator( )for the return value of operator->* passing in
,
the arguments that were given to operator->* The
.
FunctionObject::operator( takes the arguments and then
)
dereferences the "real" pointer-to-member using its stored object
pointer and pointer-to-member.
Notice that what you are doing here, just as with operator-> is
,
inserting yourself in the middle of the call to operator->* This
.
allows you to perform some extra operations if you need to.
The operator->*mechanism implemented here only works for
member functions that take an int argument and return an int. This
is limiting, but if you try to create overloaded mechanisms for each
different possibility, it seems like a prohibitive task. Fortunately,
12: Operator Overloading
543
img
C++'s templatemechanism (described in the last chapter of this
book, and in Volume 2) is designed to handle just such a problem.
Operators you can't overload
There are certain operators in the available set that cannot be
overloaded. The general reason for the restriction is safety. If these
operators were overloadable, it would somehow jeopardize or
break safety mechanisms, make things harder, or confuse existing
practice.
·
The member selection operator. Currently, the dot has a
.
meaning for any member in a class, but if you allow it to be
overloaded, then you couldn't access members in the normal
way; instead you'd have to use a pointer and the arrow
operator->
.
·
The pointer to member dereference operator.* for the same
,
reason as operator.
.
·
There's no exponentiation operator. The most popular choice
for this was operator**from Fortran, but this raised difficult
parsing questions. Also, C has no exponentiation operator, so
C++ didn't seem to need one either because you can always
perform a function call. An exponentiation operator would add
a convenient notation, but no new language functionality to
account for the added complexity of the compiler.
·
There are no user-defined operators. That is, you can't make up
new operators that aren't currently in the set. Part of the
problem is how to determine precedence, and part of the
problem is an insufficient need to account for the necessary
trouble.
·
You can't change the precedence rules. They're hard enough to
remember as it is without letting people play with them.
544
Thinking in C++
img
Non-member operators
In some of the previous examples, the operators may be members
or non-members, and it doesn't seem to make much difference.
This usually raises the question, "Which should I choose?" In
general, if it doesn't make any difference, they should be members,
to emphasize the association between the operator and its class.
When the left-hand operand is always an object of the current class,
this works fine.
However, sometimes you want the left-hand operand to be an
object of some other class. A common place you'll see this is when
the operators << and >> are overloaded for iostreams. Since
iostreams is a fundamental C++ library, you'll probably want to
overload these operators for most of your classes, so the process is
worth memorizing:
//: C12:IostreamOperatorOverloading.cpp
// Example of non-member overloaded operators
#include "../require.h"
#include <iostream>
#include <sstream> // "String streams"
#include <cstring>
using namespace std;
class IntArray {
enum { sz = 5 };
int i[sz];
public:
IntArray() { memset(i, 0, sz* sizeof(*i)); }
int& operator[](int x) {
require(x >= 0 && x < sz,
"IntArray::operator[] out of range");
return i[x];
}
friend ostream&
operator<<(ostream& os, const IntArray& ia);
friend istream&
operator>>(istream& is, IntArray& ia);
};
12: Operator Overloading
545
img
ostream&
operator<<(ostream& os, const IntArray& ia) {
for(int j = 0; j < ia.sz; j++) {
os << ia.i[j];
if(j != ia.sz -1)
os << ", ";
}
os << endl;
return os;
}
istream& operator>>(istream& is, IntArray& ia){
for(int j = 0; j < ia.sz; j++)
is >> ia.i[j];
return is;
}
int main() {
stringstream input("47 34 56 92 103");
IntArray I;
input >> I;
I[4] = -1; // Use overloaded operator[]
cout << I;
} ///:~
This class also contains an overloaded operator [ ] which returns a
,
reference to a legitimate value in the array. Because a reference is
returned, the expression
I[4] = -1;
not only looks much more civilized than if pointers were used, it
also accomplishes the desired effect.
It's important that the overloaded shift operators pass and return
by reference, so the actions will affect the external objects. In the
function definitions, expressions like
os << ia.i[j];
cause the existing overloaded operator functions to be called (that
is, those defined in <iostream> In this case, the function called is
).
546
Thinking in C++
img
.
ostream& operator<<(ostream&, int)
because ia.i[j] resolves to an
int.
Once all the actions are performed on the istream or ostream, it is
returned so it can be used in a more complicated expression.
In main( ), a new type of iostreamis used: the stringstream
(declared in <sstream> This is a class that takes a string (which it
).
can create from a char array, as shown here) and turns it into an
iostream In the example above, this means that the shift operators
.
can be tested without opening a file or typing data in on the
command line.
The form shown in this example for the inserter and extractor is
standard. If you want to create these operators for your own class,
copy the function signatures and return types above and follow the
form of the body.
Basic guidelines
Murray1 suggests these guidelines for choosing between members
and non-members:
Operator
Recommended use
All unary operators
member
= ( ) [ ] ­> ­>*
must be member
+= ­= /= *= ^=
member
&= |= %= >>= <<=
All other binary
non-member
operators
1 Rob Murray, C++ Strategies & Tactics, Addison-Wesley, 1993, page 47.
12: Operator Overloading
547
img
Overloading assignment
A common source of confusion with new C++ programmers is
assignment. This is no doubt because the = sign is such a
fundamental operation in programming, right down to copying a
register at the machine level. In addition, the copy-constructor
(described in Chapter 11) is also sometimes invoked when the =
sign is used:
MyType b;
MyType a = b;
a = b;
In the second line, the object a is being defined. A new object is
being created where one didn't exist before. Because you know by
now how defensive the C++ compiler is about object initialization,
you know that a constructor must always be called at the point
where an object is defined. But which constructor? a is being
created from an existing MyType object (b, on the right side of the
equal sign), so there's only one choice: the copy-constructor. Even
though an equal sign is involved, the copy-constructor is called.
In the third line, things are different. On the left side of the equal
sign, there's a previously initialized object. Clearly, you don't call a
constructor for an object that's already been created. In this case
MyType::operator= called for a, taking as an argument whatever
is
appears on the right-hand side. (You can have multiple operator=
functions to take different types of right-hand arguments.)
This behavior is not restricted to the copy-constructor. Any time
you're initializing an object using an = instead of the ordinary
function-call form of the constructor, the compiler will look for a
constructor that accepts whatever is on the right-hand side:
//: C12:CopyingVsInitialization.cpp
class Fi {
public:
Fi() {}
};
548
Thinking in C++
img
class Fee {
public:
Fee(int) {}
Fee(const Fi&) {}
};
int main() {
Fee fee = 1; // Fee(int)
Fi fi;
Fee fum = fi; // Fee(Fi)
} ///:~
When dealing with the = sign, it's important to keep this distinction
in mind: If the object hasn't been created yet, initialization is
required; otherwise the assignment operator=is used.
It's even better to avoid writing code that uses the = for
initialization; instead, always use the explicit constructor form. The
two constructions with the equal sign then become:
Fee fee(1);
Fee fum(fi);
This way, you'll avoid confusing your readers.
Behavior of operator=
In Integer.hand Byte.h, you saw that operator=can be only a
member function. It is intimately connected to the object on the left
side of the `='. If it was possible to define operator=globally, then
you might attempt to redefine the built-in `=' sign:
int operator=(int, MyType); // Global = not allowed!
The compiler skirts this whole issue by forcing you to make
operator=a member function.
When you create an operator= you must copy all of the necessary
,
information from the right-hand object into the current object (that
is, the object that operator=is being called for) to perform whatever
12: Operator Overloading
549
img
you consider "assignment" for your class. For simple objects, this is
obvious:
//: C12:SimpleAssignment.cpp
// Simple operator=()
#include <iostream>
using namespace std;
class Value {
int a, b;
float c;
public:
Value(int aa = 0, int bb = 0, float cc = 0.0)
: a(aa), b(bb), c(cc) {}
Value& operator=(const Value& rv) {
a = rv.a;
b = rv.b;
c = rv.c;
return *this;
}
friend ostream&
operator<<(ostream& os, const Value& rv) {
return os << "a = " << rv.a << ", b = "
<< rv.b << ", c = " << rv.c;
}
};
int main() {
Value a, b(1, 2,
3.3);
cout << "a: " <<
a << endl;
cout << "b: " <<
b << endl;
a = b;
cout << "a after
assignment: " << a << endl;
} ///:~
Here, the object on the left side of the = copies all the elements of
the object on the right, then returns a reference to itself, which
allows a more complex expression to be created.
This example includes a common mistake. When you're assigning
two objects of the same type, you should always check first for self-
assignment: is the object being assigned to itself? In some cases,
such as this one, it's harmless if you perform the assignment
550
Thinking in C++
img
operations anyway, but if changes are made to the implementation
of the class, it can make a difference, and if you don't do it as a
matter of habit, you may forget and cause hard-to-find bugs.
Pointers in classes
What happens if the object is not so simple? For example, what if
the object contains pointers to other objects? Simply copying a
pointer means that you'll end up with two objects pointing to the
same storage location. In situations like these, you need to do
bookkeeping of your own.
There are two common approaches to this problem. The simplest
technique is to copy whatever the pointer refers to when you do an
assignment or a copy-construction. This is straightforward:
//: C12:CopyingWithPointers.cpp
// Solving the pointer aliasing problem by
// duplicating what is pointed to during
// assignment and copy-construction.
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;
class Dog {
string nm;
public:
Dog(const string& name) : nm(name) {
cout << "Creating Dog: " << *this << endl;
}
// Synthesized copy-constructor & operator=
// are correct.
// Create a Dog from a Dog pointer:
Dog(const Dog* dp, const string& msg)
: nm(dp->nm + msg) {
cout << "Copied dog " << *this << " from "
<< *dp << endl;
}
~Dog() {
cout << "Deleting Dog: " << *this << endl;
}
void rename(const string& newName) {
12: Operator Overloading
551
img
nm = newName;
cout << "Dog renamed to: " << *this << endl;
}
friend ostream&
operator<<(ostream& os, const Dog& d) {
return os << "[" << d.nm << "]";
}
};
class DogHouse {
Dog* p;
string houseName;
public:
DogHouse(Dog* dog, const string& house)
: p(dog), houseName(house) {}
DogHouse(const DogHouse& dh)
: p(new Dog(dh.p, " copy-constructed")),
houseName(dh.houseName
+ " copy-constructed") {}
DogHouse& operator=(const DogHouse& dh) {
// Check for self-assignment:
if(&dh != this) {
p = new Dog(dh.p, " assigned");
houseName = dh.houseName + " assigned";
}
return *this;
}
void renameHouse(const string& newName) {
houseName = newName;
}
Dog* getDog() const { return p; }
~DogHouse() { delete p; }
friend ostream&
operator<<(ostream& os, const DogHouse& dh) {
return os << "[" << dh.houseName
<< "] contains " << *dh.p;
}
};
int main() {
DogHouse fidos(new Dog("Fido"), "FidoHouse");
cout << fidos << endl;
DogHouse fidos2 = fidos; // Copy construction
cout << fidos2 << endl;
fidos2.getDog()->rename("Spot");
552
Thinking in C++
img
fidos2.renameHouse("SpotHouse");
cout << fidos2 << endl;
fidos = fidos2; // Assignment
cout << fidos << endl;
fidos.getDog()->rename("Max");
fidos2.renameHouse("MaxHouse");
} ///:~
Dog is a simple class that contains only a string that holds the
name of the dog. However, you'll generally know when something
happens to a Dog because the constructors and destructors print
information when they are called. Notice that the second
constructor is a bit like a copy-constructor except that it takes a
pointer to a Dog instead of a reference, and it has a second
argument that is a message that's concatenated to the argument
Dog's name. This is used to help trace the behavior of the program.
You can see that whenever a member function prints information, it
doesn't access that information directly but instead sends *this to
cout. This in turn calls the ostream operator<< It's valuable to do it
.
this way because if you want to reformat the way that Dog
information is displayed (as I did by adding the `[' and `]') you only
need to do it in one place.
A DogHousecontains a Dog* and demonstrates the four functions
you will always need to define when your class contains pointers:
all necessary ordinary constructors, the copy-constructor,
operator=(either define it or disallow it), and a destructor. The
operator=checks for self-assignment as a matter of course, even
though it's not strictly necessary here. This virtually eliminates the
possibility that you'll forget to check for self-assignment if you do
change the code so that it matters.
Reference Counting
In the example above, the copy-constructor and operator=make a
new copy of what the pointer points to, and the destructor deletes
it. However, if your object requires a lot of memory or a high
initialization overhead, you may want to avoid this copying. A
12: Operator Overloading
553
img
common approach to this problem is called reference counting. You
give intelligence to the object that's being pointed to so it knows
how many objects are pointing to it. Then copy-construction or
assignment means attaching another pointer to an existing object
and incrementing the reference count. Destruction means reducing
the reference count and destroying the object if the reference count
goes to zero.
But what if you want to write to the object (the Dog in the example
above)? More than one object may be using this Dog, so you'd be
modifying someone else's Dog as well as yours, which doesn't
seem very neighborly. To solve this "aliasing" problem, an
additional technique called copy-on-write is used. Before writing to a
block of memory, you make sure no one else is using it. If the
reference count is greater than one, you must make yourself a
personal copy of that block before writing it, so you don't disturb
someone else's turf. Here's a simple example of reference counting
and copy-on-write:
//: C12:ReferenceCounting.cpp
// Reference count, copy-on-write
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;
class Dog {
string nm;
int refcount;
Dog(const string& name)
: nm(name), refcount(1) {
cout << "Creating Dog: " << *this << endl;
}
// Prevent assignment:
Dog& operator=(const Dog& rv);
public:
// Dogs can only be created on the heap:
static Dog* make(const string& name) {
return new Dog(name);
}
554
Thinking in C++
img
Dog(const Dog& d)
: nm(d.nm + " copy"), refcount(1) {
cout << "Dog copy-constructor: "
<< *this << endl;
}
~Dog() {
cout << "Deleting Dog: " << *this << endl;
}
void attach() {
++refcount;
cout << "Attached Dog: " << *this << endl;
}
void detach() {
require(refcount != 0);
cout << "Detaching Dog: " << *this << endl;
// Destroy object if no one is using it:
if(--refcount == 0) delete this;
}
// Conditionally copy this Dog.
// Call before modifying the Dog, assign
// resulting pointer to your Dog*.
Dog* unalias() {
cout << "Unaliasing Dog: " << *this << endl;
// Don't duplicate if not aliased:
if(refcount == 1) return this;
--refcount;
// Use copy-constructor to duplicate:
return new Dog(*this);
}
void rename(const string& newName) {
nm = newName;
cout << "Dog renamed to: " << *this << endl;
}
friend ostream&
operator<<(ostream& os, const Dog& d) {
return os << "[" << d.nm << "], rc = "
<< d.refcount;
}
};
class DogHouse {
Dog* p;
string houseName;
public:
DogHouse(Dog* dog, const string& house)
12: Operator Overloading
555
img
: p(dog), houseName(house) {
cout << "Created DogHouse: "<< *this << endl;
}
DogHouse(const DogHouse& dh)
: p(dh.p),
houseName("copy-constructed " +
dh.houseName) {
p->attach();
cout << "DogHouse copy-constructor: "
<< *this << endl;
}
DogHouse& operator=(const DogHouse& dh) {
// Check for self-assignment:
if(&dh != this) {
houseName = dh.houseName + " assigned";
// Clean up what you're using first:
p->detach();
p = dh.p; // Like copy-constructor
p->attach();
}
cout << "DogHouse operator= : "
<< *this << endl;
return *this;
}
// Decrement refcount, conditionally destroy
~DogHouse() {
cout << "DogHouse destructor: "
<< *this << endl;
p->detach();
}
void renameHouse(const string& newName) {
houseName = newName;
}
void unalias() { p = p->unalias(); }
// Copy-on-write. Anytime you modify the
// contents of the pointer you must
// first unalias it:
void renameDog(const string& newName) {
unalias();
p->rename(newName);
}
// ... or when you allow someone else access:
Dog* getDog() {
unalias();
return p;
556
Thinking in C++
img
}
friend ostream&
operator<<(ostream& os, const DogHouse& dh) {
return os << "[" << dh.houseName
<< "] contains " << *dh.p;
}
};
int main() {
DogHouse
fidos(Dog::make("Fido"), "FidoHouse"),
spots(Dog::make("Spot"), "SpotHouse");
cout << "Entering copy-construction" << endl;
DogHouse bobs(fidos);
cout << "After copy-constructing bobs" << endl;
cout << "fidos:" << fidos << endl;
cout << "spots:" << spots << endl;
cout << "bobs:" << bobs << endl;
cout << "Entering spots = fidos" << endl;
spots = fidos;
cout << "After spots = fidos" << endl;
cout << "spots:" << spots << endl;
cout << "Entering self-assignment" << endl;
bobs = bobs;
cout << "After self-assignment" << endl;
cout << "bobs:" << bobs << endl;
// Comment out the following lines:
cout << "Entering rename(\"Bob\")" << endl;
bobs.getDog()->rename("Bob");
cout << "After rename(\"Bob\")" << endl;
} ///:~
The class Dog is the object pointed to by a DogHouse It contains a
.
reference count and functions to control and read the reference
count. There's a copy-constructor so you can make a new Dog from
an existing one.
The attach( )function increments the reference count of a Dog to
indicate there's another object using it. detach( )decrements the
reference count. If the reference count goes to zero, then no one is
using it anymore, so the member function destroys its own object
by saying delete this
.
12: Operator Overloading
557
img
Before you make any modifications (such as renaming a Dog), you
should ensure that you aren't changing a Dog that some other
object is using. You do this by calling DogHouse::unalias( ,)which
in turn calls Dog::unalias( .) The latter function will return the
existing Dog pointer if the reference count is one (meaning no one
else is pointing to that Dog), but will duplicate the Dog if the
reference count is more than one.
The copy-constructor, instead of creating its own memory, assigns
Dog to the Dog of the source object. Then, because there's now an
additional object using that block of memory, it increments the
reference count by calling Dog::attach( .)
The operator=deals with an object that has already been created on
the left side of the =, so it must first clean that up by calling
detach( )for that Dog, which will destroy the old Dog if no one else
is using it. Then operator=repeats the behavior of the copy-
constructor. Notice that it first checks to detect whether you're
assigning the same object to itself.
The destructor calls detach( )to conditionally destroy the Dog.
To implement copy-on-write, you must control all the actions that
write to your block of memory. For example, the renameDog( )
member function allows you to change the values in the block of
memory. But first, it uses unalias( )to prevent the modification of
an aliased Dog (a Dog with more than one DogHouseobject
pointing to it). And if you need to produce a pointer to a Dog from
within a DogHouse you unalias( )that pointer first.
,
main( ) tests the various functions that must work correctly to
implement reference counting: the constructor, copy-constructor,
operator= and destructor. It also tests the copy-on-write by calling
,
renameDog( )
.
Here's the output (after a little reformatting):
558
Thinking in C++
img
Creating Dog: [Fido], rc = 1
Created DogHouse: [FidoHouse]
contains [Fido], rc = 1
Creating Dog: [Spot], rc = 1
Created DogHouse: [SpotHouse]
contains [Spot], rc = 1
Entering copy-construction
Attached Dog: [Fido], rc = 2
DogHouse copy-constructor:
[copy-constructed FidoHouse]
contains [Fido], rc = 2
After copy-constructing bobs
fidos:[FidoHouse] contains [Fido], rc = 2
spots:[SpotHouse] contains [Spot], rc = 1
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 2
Entering spots = fidos
Detaching Dog: [Spot], rc = 1
Deleting Dog: [Spot], rc = 0
Attached Dog: [Fido], rc = 3
DogHouse operator= : [FidoHouse assigned]
contains [Fido], rc = 3
After spots = fidos
spots:[FidoHouse assigned] contains [Fido],rc = 3
Entering self-assignment
DogHouse operator= : [copy-constructed FidoHouse]
contains [Fido], rc = 3
After self-assignment
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 3
Entering rename("Bob")
After rename("Bob")
DogHouse destructor: [copy-constructed FidoHouse]
contains [Fido], rc = 3
Detaching Dog: [Fido], rc = 3
DogHouse destructor: [FidoHouse assigned]
contains [Fido], rc = 2
Detaching Dog: [Fido], rc = 2
DogHouse destructor: [FidoHouse]
contains [Fido], rc = 1
Detaching Dog: [Fido], rc = 1
Deleting Dog: [Fido], rc = 0
12: Operator Overloading
559
img
By studying the output, tracing through the source code, and
experimenting with the program, you'll deepen your
understanding of these techniques.
Automatic operator= creation
Because assigning an object to another object of the same type is an
activity most people expect to be possible, the compiler will
automatically create a type::operator=(type) you don't make one.
if
The behavior of this operator mimics that of the automatically
created copy-constructor; if the class contains objects (or is
inherited from another class), the operator=for those objects is
called recursively. This is called memberwise assignment. For
example,
//: C12:AutomaticOperatorEquals.cpp
#include <iostream>
using namespace std;
class Cargo {
public:
Cargo& operator=(const Cargo&) {
cout << "inside Cargo::operator=()" << endl;
return *this;
}
};
class Truck {
Cargo b;
};
int main() {
Truck a, b;
a = b; // Prints: "inside Cargo::operator=()"
} ///:~
The automatically generated operator=for Truck calls
Cargo::operator=
.
In general, you don't want to let the compiler do this for you. With
classes of any sophistication (especially if they contain pointers!)
you want to explicitly create an operator= If you really don't want
.
560
Thinking in C++
img
people to perform assignment, declare operator=as a private
function. (You don't need to define it unless you're using it inside
the class.)
Automatic type conversion
In C and C++, if the compiler sees an expression or function call
using a type that isn't quite the one it needs, it can often perform an
automatic type conversion from the type it has to the type it wants.
In C++, you can achieve this same effect for user-defined types by
defining automatic type conversion functions. These functions
come in two flavors: a particular type of constructor and an
overloaded operator.
Constructor conversion
If you define a constructor that takes as its single argument an
object (or reference) of another type, that constructor allows the
compiler to perform an automatic type conversion. For example,
//: C12:AutomaticTypeConversion.cpp
// Type conversion constructor
class One {
public:
One() {}
};
class Two {
public:
Two(const One&) {}
};
void f(Two) {}
int main() {
One one;
f(one); // Wants a Two, has a One
} ///:~
12: Operator Overloading
561
img
When the compiler sees f( ) called with a One object, it looks at the
declaration for f( ) and notices it wants a Two. Then it looks to see
if there's any way to get a Two from a One, and it finds the
constructor Two::Two(One) which it quietly calls. The resulting
,
Two object is handed to f( ).
In this case, automatic type conversion has saved you from the
trouble of defining two overloaded versions of f( ). However, the
cost is the hidden constructor call to Two, which may matter if
you're concerned about the efficiency of calls to f( ).
Preventing constructor conversion
There are times when automatic type conversion via the
constructor can cause problems. To turn it off, you modify the
constructor by prefacing with the keyword explicit(which only
works with constructors). Used to modify the constructor of class
Two in the example above:
//: C12:ExplicitKeyword.cpp
// Using the "explicit" keyword
class One {
public:
One() {}
};
class Two {
public:
explicit Two(const One&) {}
};
void f(Two) {}
int main() {
One one;
//!  f(one); // No auto conversion allowed
f(Two(one)); // OK -- user performs conversion
} ///:~
By making Two's constructor explicit, the compiler is told not to
perform any automatic conversion using that particular constructor
562
Thinking in C++
img
(other non-explicitconstructors in that class can still perform
automatic conversions). If the user wants to make the conversion
happen, the code must be written out. In the code above,
f(Two(one))creates a temporary object of type Two from one, just
like the compiler did in the previous version.
Operator conversion
The second way to produce automatic type conversion is through
operator overloading. You can create a member function that takes
the current type and converts it to the desired type using the
operatorkeyword followed by the type you want to convert to.
This form of operator overloading is unique because you don't
appear to specify a return type ­ the return type is the name of the
operator you're overloading. Here's an example:
//: C12:OperatorOverloadingConversion.cpp
class Three {
int i;
public:
Three(int ii = 0, int = 0) : i(ii) {}
};
class Four {
int x;
public:
Four(int xx) : x(xx) {}
operator Three() const { return Three(x); }
};
void g(Three) {}
int main() {
Four four(1);
g(four);
g(1);  // Calls Three(1,0)
} ///:~
With the constructor technique, the destination class is performing
the conversion, but with operators, the source class performs the
conversion. The value of the constructor technique is that you can
12: Operator Overloading
563
img
add a new conversion path to an existing system as you're creating
a new class. However, creating a single-argument constructor
always defines an automatic type conversion (even if it's got more
than one argument, if the rest of the arguments are defaulted),
which may not be what you want (in which case you can turn it off
using explicit In addition, there's no way to use a constructor
).
conversion from a user-defined type to a built-in type; this is
possible only with operator overloading.
Reflexivity
One of the most convenient reasons to use global overloaded
operators instead of member operators is that in the global
versions, automatic type conversion may be applied to either
operand, whereas with member objects, the left-hand operand must
already be the proper type. If you want both operands to be
converted, the global versions can save a lot of coding. Here's a
small example:
//: C12:ReflexivityInOverloading.cpp
class Number {
int i;
public:
Number(int ii = 0) : i(ii) {}
const Number
operator+(const Number& n) const {
return Number(i + n.i);
}
friend const Number
operator-(const Number&, const Number&);
};
const Number
operator-(const Number& n1,
const Number& n2) {
return Number(n1.i - n2.i);
}
int main() {
Number a(47), b(11);
a + b; // OK
a + 1; // 2nd arg converted to Number
564
Thinking in C++
img
//! 1 + a; // Wrong! 1st arg not of type Number
a - b; // OK
a - 1; // 2nd arg converted to Number
1 - a; // 1st arg converted to Number
} ///:~
Class Number has both a member operator+and a friend
operator­ Because there's a constructor that takes a single int
.
argument, an int can be automatically converted to a Number, but
only under the right conditions. In main( ), you can see that adding
a Number to another Number works fine because it's an exact
match to the overloaded operator. Also, when the compiler sees a
Number followed by a + and an int, it can match to the member
function Number::operator+
and convert the int argument to a
Number using the constructor. But when it sees an int, a +, and a
Number, it doesn't know what to do because all it has is
Number::operator+which requires that the left operand already
,
be a Number object. Thus, the compiler issues an error.
With the friend operator­ things are different. The compiler needs
,
to fill in both its arguments however it can; it isn't restricted to
having a Number as the left-hand argument. Thus, if it sees
1­a
it can convert the first argument to a Number using the
constructor.
Sometimes you want to be able to restrict the use of your operators
by making them members. For example, when multiplying a matrix
by a vector, the vector must go on the right. But if you want your
operators to be able to convert either argument, make the operator
a friend function.
Fortunately, the compiler will not take 1 ­ 1 and convert both
arguments to Number objects and then call operator­ That would
.
mean that existing C code might suddenly start to work differently.
The compiler matches the "simplest" possibility first, which is the
built-in operator for the expression 1 ­ 1.
12: Operator Overloading
565
img
Type conversion example
An example in which automatic type conversion is extremely
helpful occurs with any class that encapsulates character strings (in
this case, we will just implement the class using the Standard C++
string class because it's simple). Without automatic type
conversion, if you want to use all the existing string functions from
the Standard C library, you have to create a member function for
each one, like this:
//: C12:Strings1.cpp
// No auto type conversion
#include "../require.h"
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std;
class Stringc {
string s;
public:
Stringc(const string& str = "") : s(str) {}
int strcmp(const Stringc& S) const {
return ::strcmp(s.c_str(), S.s.c_str());
}
// ... etc., for every function in string.h
};
int main() {
Stringc s1("hello"), s2("there");
s1.strcmp(s2);
} ///:~
Here, only the strcmp( )function is created, but you'd have to
create a corresponding function for every one in <cstring>that
might be needed. Fortunately, you can provide an automatic type
conversion allowing access to all the functions in <cstring>
:
//: C12:Strings2.cpp
// With auto type conversion
#include "../require.h"
#include <cstring>
#include <cstdlib>
566
Thinking in C++
img
#include <string>
using namespace std;
class Stringc {
string s;
public:
Stringc(const string& str = "") : s(str) {}
operator const char*() const {
return s.c_str();
}
};
int main() {
Stringc s1("hello"), s2("there");
strcmp(s1, s2); // Standard C function
strspn(s1, s2); // Any string function!
} ///:~
Now any function that takes a char* argument can also take a
Stringc argument because the compiler knows how to make a char*
from a Stringc.
Pitfalls in automatic type conversion
Because the compiler must choose how to quietly perform a type
conversion, it can get into trouble if you don't design your
conversions correctly. A simple and obvious situation occurs with a
class X that can convert itself to an object of class Y with an
operator Y( . If class Y has a constructor that takes a single
)
argument of type X, this represents the identical type conversion.
The compiler now has two ways to go from X to Y, so it will
generate an ambiguity error when that conversion occurs:
//: C12:TypeConversionAmbiguity.cpp
class Orange; // Class declaration
class Apple {
public:
operator Orange() const; // Convert Apple to Orange
};
class Orange {
12: Operator Overloading
567
img
public:
Orange(Apple); // Convert Apple to Orange
};
void f(Orange) {}
int main() {
Apple a;
//! f(a); // Error: ambiguous conversion
} ///:~
The obvious solution to this problem is not to do it. Just provide a
single path for automatic conversion from one type to another.
A more difficult problem to spot occurs when you provide
automatic conversion to more than one type. This is sometimes
called fan-out:
//: C12:TypeConversionFanout.cpp
class Orange {};
class Pear {};
class Apple {
public:
operator Orange() const;
operator Pear() const;
};
// Overloaded eat():
void eat(Orange);
void eat(Pear);
int main() {
Apple c;
//! eat(c);
// Error: Apple -> Orange or Apple -> Pear ???
} ///:~
Class Apple has automatic conversions to both Orange and Pear.
The insidious thing about this is that there's no problem until
someone innocently comes along and creates two overloaded
versions of eat( ). (With only one version, the code in main( ) works
fine.)
568
Thinking in C++
img
Again, the solution ­ and the general watchword with automatic
type conversion ­ is to provide only a single automatic conversion
from one type to another. You can have conversions to other types;
they just shouldn't be automatic. You can create explicit function
calls with names like makeA( )and makeB( )
.
Hidden activities
Automatic type conversion can introduce more underlying
activities than you may expect. As a little brain teaser, look at this
modification of CopyingVsInitialization.cpp
:
//: C12:CopyingVsInitialization2.cpp
class Fi {};
class Fee {
public:
Fee(int) {}
Fee(const Fi&) {}
};
class Fo {
int i;
public:
Fo(int x = 0) : i(x) {}
operator Fee() const { return Fee(i); }
};
int main() {
Fo fo;
Fee fee = fo;
} ///:~
There is no constructor to create the Fee fee from a Fo object.
However, Fo has an automatic type conversion to a Fee. There's no
copy-constructor to create a Fee from a Fee, but this is one of the
special functions the compiler can create for you. (The default
constructor, copy-constructor, operator= and destructor can be
,
synthesized automatically by the compiler.) So for the relatively
innocuous statement
Fee fee = fo;
12: Operator Overloading
569
img
the automatic type conversion operator is called, and a copy-
constructor is created.
Use automatic type conversion carefully. As with all operator
overloading, it's excellent when it significantly reduces a coding
task, but it's usually not worth using gratuitously.
Summary
The whole reason for the existence of operator overloading is for
those situations when it makes life easier. There's nothing
particularly magical about it; the overloaded operators are just
functions with funny names, and the function calls happen to be
made for you by the compiler when it spots the right pattern. But if
operator overloading doesn't provide a significant benefit to you
(the creator of the class) or the user of the class, don't confuse the
issue by adding it.
Exercises
Solutions to selected exercises can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from .
1.
Create a simple class with an overloaded operator++ Try
.
calling this operator in both pre- and postfix form and
see what kind of compiler warning you get.
2.
Create a simple class containing an int and overload the
operator+as a member function. Also provide a print( )
member function that takes an ostream&as an argument
and prints to that ostream& Test your class to show that
.
it works correctly.
3.
Add a binary operator-to Exercise 2 as a member
function. Demonstrate that you can use your objects in
complex expressions like
a+b­c
.
570
Thinking in C++
img
4.
Add an operator++and operator--to Exercise 2, both the
prefix and the postfix versions, such that they return the
incremented or decremented object. Make sure that the
postfix versions return the correct value.
5.
Modify the increment and decrement operators in
Exercise 4 so that the prefix versions return a non-const
reference and the postfix versions return a const object.
Show that they work correctly and explain why this
would be done in practice.
6.
Change the print( )function in Exercise 2 so that it is the
overloaded operator<<as in
IostreamOperatorOverloading.cpp
.
7.
Modify Exercise 3 so that the operator+and operator-are
non-member functions. Demonstrate that they still work
correctly.
8.
Add the unary operator-to Exercise 2 and demonstrate
that it works correctly.
9.
Create a class that contains a single private char.
Overload the iostream operators << and >> (as in
IostreamOperatorOverloading.cpp
) and test them. You
can test them with fstreams stringstream and cin and
,
s,
cout.
10.
Determine the dummy constant value that your compiler
passes for postfix operator++and operator--
.
11.
Write a Number class that holds a double, and add
overloaded operators for +, ­, *, / and assignment.
,
Choose the return values for these functions so that
expressions can be chained together, and for efficiency.
Write an automatic type conversion operator int( .)
12.
Modify Exercise 11 so that the return value optimization is
used, if you have not already done so.
13.
Create a class that contains a pointer, and demonstrate
that if you allow the compiler to synthesize the operator=
the result of using that operator will be pointers that are
aliased to the same storage. Now fix the problem by
12: Operator Overloading
571
img
defining your own operator=and demonstrate that it
corrects the aliasing. Make sure you check for self-
assignment and handle that case properly.
14.
Write a class called Bird that contains a string member
and a static int In the default constructor, use the int to
.
automatically generate an identifier that you build in the
string, along with the name of the class (Bird #1, Bird #2,
etc.). Add an operator<<for ostreams to print out the
Bird objects. Write an assignment operator=and a copy-
constructor. In main( ), verify that everything works
correctly.
15.
Write a class called BirdHousethat contains an object, a
pointer and a reference for class Bird from Exercise 14.
The constructor should take the three Birds as
arguments. Add an operator<<for ostreams for
BirdHouse Disallow the assignment operator=and
.
copy-constructor. In main( ), verify that everything
works correctly. Make sure that you can chain
assignments for BirdHouseobjects and build expressions
involving multiple operators.
16.
Add an int data member to both Bird and BirdHousein
Exercise 15. Add member operators +, -, *, and / that use
the int members to perform the operations on the
respective members. Verify that these work.
17.
Repeat Exercise 16 using non-member operators.
18.
Add an operator--to SmartPointer.cpp
and
NestedSmartPointer.cpp
.
19.
Modify CopyingVsInitialization.cpp that all of the
so
constructors print a message that tells you what's going
on. Now verify that the two forms of calls to the copy-
constructor (the assignment form and the parenthesized
form) are equivalent.
20.
Attempt to create a non-member operator=for a class
and see what kind of compiler message you get.
21.
Create a class with an assignment operator that has a
second argument, a string that has a default value that
572
Thinking in C++
img
says "op= call." Create a function that assigns an object
of your class to another one and show that your
assignment operator is called correctly.
22.
In CopyingWithPointers.cppremove the operator=in
,
DogHouseand show that the compiler-synthesized
operator=correctly copies the string but simply aliases
the Dog pointer.
23.
In ReferenceCounting.cppadd a static intand an
,
ordinary int as data members to both Dog and
DogHouse In all constructors for both classes, increment
.
the static intand assign the result to the ordinary int to
keep track of the number of objects that have been
created. Make the necessary modifications so that all the
printing statements will say the int identifiers of the
objects involved.
24.
Create a class containing a string as a data member.
Initialize the string in the constructor, but do not create a
copy-constructor or operator= Make a second class that
.
has a member object of your first class; do not create a
copy-constructor or operator=for this class either.
Demonstrate that the copy-constructor and operator=are
properly synthesized by the compiler.
25.
Combine the classes in OverloadingUnaryOperators.cpp
and Integer.cpp
.
26.
Modify PointerToMemberOperator.cpp adding two
by
new member functions to Dog that take no arguments
and return void. Create and test an overloaded
operator->*that works with your two new functions.
27.
Add an operator->*to NestedSmartPointer.cpp
.
28.
Create two classes, Apple and Orange. In Apple, create a
constructor that takes an Orange as an argument. Create
a function that takes an Apple and call that function with
an Orange to show that it works. Now make the Apple
constructor explicitto demonstrate that the automatic
type conversion is thus prevented. Modify the call to
12: Operator Overloading
573
img
your function so that the conversion is made explicitly
and thus succeeds.
29.
Add a global operator*to ReflexivityInOverloading.cpp
and demonstrate that it is reflexive.
30.
Create two classes and create an operator+and the
conversion functions such that addition is reflexive for
the two classes.
31.
Fix TypeConversionFanout.cpp creating an explicit
by
function to call to perform the type conversion, instead of
one of the automatic conversion operators.
32.
Write simple code that uses the +, -, *, and / operators for
doubles. Figure out how your compiler generates
assembly code and look at the assembly language that's
generated to discover and explain what's going on under
the hood.
574
Thinking in C++