course of C++ programming language

lecture 7, 8: operator overloading


Friends

C++ allows classes to declare that other classes or nonmember functions are friends, and can access protected and private data members and methods.

class Person { friend void print (ostream &out, const Person &pers); private: string firstname, surname; int age; // ... }; void print (ostream &out, const Person &pers) { out << pers.firstname << " " << pers.surname << " is " << pers.age; }

You can specify that one or more functions or members of another class are friends.

An ordinary member function declaration specifies three logically distinct things:
[1] the function can access the private part of the class declaration, and
[2] the function is in the scope of the class, and
[3] the function must be invoked on an object (has a this pointer).
By declaring a member function static, we can give it the first two properties only. By declaring a function a friend, we can give it the first property only.

Fundamentals of operators

C++ supports a set of operators for its built-in types. However, most concepts for which operators are conventionally used are not built-in types in C++, so they must be represented as user-defined types. For example, if you need complex arithmetic, matrix algebra, logic signals, or character strings in C++, you use classes to represent these notions. Defining operators for such classes sometimes allows a programmer to provide a more conventional and convenient notation for manipulating objects than could be achieved using only the basic functional notation.

class Complex { double re, im; public: Complex (double r=0, double i=0) : re(r), im(i) {} public: Complex operator+ (Complex); // addition Complex operator* (Complex); // multiplication // ... }; Complex Complex::operator+ (Complex c) { return Complex(re+c.re,im+c.im); } Complex Complex::operator* (Complex c) { return Complex(re*c.re-im*c.im,im*c.re+re*c.im); }

Above example defines a simple implementation of the concept of complex numbers. A Complex is represented by a pair of double-precision floating-point numbers manipulated by the operators + and *. The programmer defines Complex::operator+() and Complex::operator*() to provide meanings for + and *, respectively.

int main () { Complex a, b(2), c(3,4); a = b+c; a = b.operator+(c); }

The following operators cannot be defined by a user:
:: (scope resolution),
. (member selection),
.* (member selection through pointer to function),
?: (conditional operator).

Syntax
An operator function must either be a member or take at least one argument of a user-defined type.

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).

class Complex { double re, im; public: Complex (double r=0, double i=0) : re(r), im(i) {} public: Complex operator+ (Complex); Complex operator* (Complex); friend Complex operator- (Complex); // unary operator friend Complex operator- (Complex, Complex); // binary operator // ... }; Complex operator- (Complex c) { return Complex(-re,-im); } Complex operator- (Complex c1, Complex c2) { return Complex(re-c.re,im-c.im); } int main () { Complex a, b(2), c(3,4); a = b-c; a = ::operator-(b,c); c = b-5; c = 6-b; b = -a; }

The following example shows the syntax to overload all the unary and binary operators, in the form of global functions (non-member friend functions).

Binary and unary operators

A binary operator can be defined by either a nonstatic member function taking one argument or a nonmember function taking two arguments.

For any binary operator @, aa@bb can be interpreted as either aa.operator@(bb) or operator@(aa,bb). If both are defined, overload resolution determines which, if any, interpretation is used.

An unary operator, whether prefix or postfix, can be defined by either a nonstatic member function taking no arguments or a nonmember function taking one argument. For any prefix unary operator @, @aa can be interpreted as either aa.operator@() or operator@(aa). If both are defined, overload resolution determines which, if any, interpretation is used. For any postfix unary operator @, aa@ can be interpreted as either aa.operator@(int) or operator@(aa,int). If both are defined, overload resolution determines which, if any, interpretation is used.

class X { int x; public: X (int); public: void operator+ (int); // ... }; void operator+ (X, X); void operator+ (X, double); void f (X a) { a+1; // a.operator+(1) 1+a; // ::operator+(X(1),a) a+1.0; // ::operator+(a,1.0) }

An operator can be declared only for the syntax defined for it in the grammar. For example, a user cannot define a unary % or a ternary +.

Predefined meanings for operators

Only a few assumptions are made about the meaning of a user-defined operator. In particular, operator=, operator[], operator() and operator-< must be nonstatic member functions; this ensures that their first operands will be lvalues. The meanings of some built-in operators are defined to be equivalent to some combination of other operators on the same arguments.

Because of historical accident, the operators = (assignment), & (address-of), and , (sequencing) have predefined meanings when applied to class objects.

Operators and user-defined types

An operator function must either be a member or take at least one argument of a user-defined type (functions redefining the new and delete operators need not). This rule ensures that a user cannot change the meaning of an expression unless the expression contains an object of a user-defined type. In particular, it is not possible to define an operator function that operates exclusively on pointers. This ensures that C++ is extensible but not mutable (with the exception of operators =, & and , for class objects).

Overloading assignment

If t1 and t2 are objects of a class Table, t2=t1 by default means a memberwise copy of t1 into t2. Having assignment interpreted this way can cause a surprising (and usually undesired) effect when used on objects of a class with pointer members. Memberwise copy is usually the wrong semantics for copying objects containing resources managed by a constructor/destructor pair.

void h (const Table &t0) { Table t1; Table t2 = t1; // copy initialization: trouble Table t3; // ... t3 = t2; // copy assignment: trouble // ... }

Here, the Table default constructor is called twice: once for t1 and t3. It is not called for t2 because that variable was initialized by copying. However, the Table destructor is called three times: once each for t1, t2 and t3! The default interpretation of assignment is memberwise copy, so t1, t2 and t2 will, at the end of h(), each contain a pointer to the array of names allocated on the free store when t1 was created. No pointer to the array of names allocated when t3 was created remains because it was overwritten by the t3=t2 assignment. Thus, in the absence of automatic garbage collection, its storage will be lost to the program forever. On the other hand, the array created for t1 appears in t1, t2, and t3, so it will be deleted thrice. The result of that is undefined and probably disastrous.

class Table { int *array, size; public: Table (const Table &tab); Table & operator= (const Table &tab); // ... };
Increment and decrement: operator++ and operator--

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.

Subscripting: operator[]

...

Function call: operator()

...


References
  1. B.Stroustrup: The C++ programming language. Third edition.
    Section 11: operator overloading; pp. 261-298.
  2. B.Eckel: Thinking in C++. Second edition.
    Section 12: operator overloading; pp. 485-542.