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.
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.
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.
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).
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.
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.
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.
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
-
B.Stroustrup: The C++ programming language. Third edition.
Section 11: operator overloading; pp. 261-298. -
B.Eckel: Thinking in C++. Second edition.
Section 12: operator overloading; pp. 485-542.