For user-defined types (classes and enumerations), you can define alternate behavior for the C++ operators. This is called overloading the operators. You cannot define new operators, and not all operators can be overloaded. Table 5-2 lists all the operators and indicates which can be overloaded. For these, it shows whether the overload must be a member function. Overloaded operators that are implemented as member functions must be nonstatic member functions.
Operator |
Meaning |
Overloading permitted? |
Must be member function? |
---|---|---|---|
+ |
Addition, unary plus |
yes |
no |
& |
Address of |
yes |
no |
[] |
Array subscript |
yes |
yes |
&= |
Assign bitwise and |
yes |
no |
^= |
Assign bitwise exclusive or |
yes |
no |
|= |
Assign bitwise or |
yes |
no |
-= |
Assign difference |
yes |
no |
<<= |
Assign left shift |
yes |
no |
= |
Assignment |
yes |
yes |
*= |
Assign product |
yes |
no |
/= |
Assign quotient |
yes |
no |
%= |
Assign remainder |
yes |
no |
>>= |
Assign right shift |
yes |
no |
+= |
Assign sum |
yes |
no |
& |
Bitwise and |
yes |
no |
~ |
Bitwise complement |
yes |
no |
^ |
Bitwise exclusive or |
yes |
no |
| |
Bitwise or |
yes |
no |
? : |
Conditional |
no |
N/A |
new |
Create dynamic object |
yes |
no |
new[] |
Create dynamic array |
yes |
no |
-- |
Decrement |
yes |
no |
delete |
Destroy dynamic object |
yes |
no |
delete[] |
Destroy dynamic array |
yes |
no |
/ |
Division |
yes |
no |
== |
Equal |
yes |
no |
( ) |
Function call |
yes |
yes |
> |
Greater than |
yes |
no |
>= |
Greater than or equal |
yes |
no |
++ |
Increment |
yes |
no |
<< |
Left shift |
yes |
no |
< |
Less than |
yes |
no |
<= |
Less than or equal |
yes |
no |
&& |
Logical and |
yes |
no |
! |
Logical complement |
yes |
no |
|| |
Logical or |
yes |
no |
.* |
Member reference |
no |
N/A |
->* |
Member reference |
yes |
yes |
. |
Member reference |
no |
N/A |
-> |
Member reference |
yes |
yes |
* |
Multiplication, dereference |
yes |
no |
!= |
Not equal |
yes |
no |
% |
Remainder |
yes |
no |
>> |
Right shift |
yes |
no |
:: |
Scope |
no |
N/A |
, |
Serial evaluation |
yes |
no |
- |
Subtraction, negation |
yes |
no |
type |
Type conversion |
yes |
yes |
An overloaded operator is a function in which the function name has the form operator followed by the operator symbol, or in the case of a type conversion member function, a list of type specifiers (with pointer, reference, and array operators). For example, the following code declares functions to overload the ! and && operators:
enum logical { no, maybe, yes }; logical operator !(logical x); logical operator &&(logical a, logical b);
Some overloaded operators must be member functions, and others can be member or nonmember functions (as shown in Table 5-2). When you define a member function, the object is always the lefthand operand. A unary operator, therefore, takes no arguments because the one operand is the object itself. Likewise, a binary operator takes one argument: the righthand operand; the lefthand operand is the object. For a nonmember function, a unary operator takes one argument and a binary operator takes two arguments: the first argument is the lefthand operand, and the second is the righthand operand.
Use overloaded operators as you would built-in operators. You can also use function notation, in which the function name is operator followed by the operator symbol, but this usage is uncommon. You can use the function notation with built-in operators, too, but such usage is extremely uncommon. For example:
operator-(42, 10) // Same as 42 - 10 operator-(33) // Same as -33
The usual rules for resolving overloaded functions applies to overloaded operators. The only difference is that the built-in operators are added to the list of candidates along with the user-defined operators. Remember that you cannot overload operators when all the operands have fundamental types. At least one operand must have a user-defined type (class or enumeration).
Defining Commutative OperatorsWhen overloading a binary operator, consider whether the operator should be commutative (a + b is the same as b + a). If that is the case, you might need to define two overloaded operators: enum priority { idle, low, normal, high }; // Adding an integer to a priority commutes. For example: // setpriority(priority( ) + 2); // setpriority(2 + priority( )); priority operator+(priority p, int i); priority operator+(int i, priority p); // Subtracting an integer from a priority does not commute. For // example: // setpriority(priority( ) - 1); priority operator-(priority p, int i); |
A key difference between the overloaded operators and the built-in operators is that the logical && and || operators are short-circuit operators. If the expression result is known by evaluating only the left operand, the right operand is never evaluated. For overloaded operators, all operands are evaluated before a function is called, so short-circuit evaluation is impossible.
In other words, you cannot tell whether the && and || operators perform short-circuit evaluation by merely glancing at them. You must study the types of the operands. It is safest, therefore, never to overload these operators. If the operators are never overloaded, you know that they are always short-circuit operators.
You can overload the comma operator (,), but you will rarely have any reason to do so. If you do, however, you change its semantics in a subtle way. The built-in comma operator has a sequence point (see Chapter 3) between its operands, so you know that the lefthand expression is completely evaluated before the righthand expression. If you overload the operator, you lose that guarantee. The ordinary rules apply, so the operands might be evaluated in any order.
When overloading the increment and decrement operators, remember that they have two forms: prefix and postfix. To distinguish between the two forms, the postfix form takes an additional int parameter. The compiler always passes 0 as the additional argument. Example 5-24 shows one way to overload the increment operator. (Decrement is analogous.)
enum status { stopped, running, waiting }; status& operator++(status& s) { // Prefix if (s != waiting) s = status(s + 1); return s; } status operator++(status& s, int) { // Postfix status rtn = s; ++s; return rtn; } int main( ) { status s(stopped); ++s; // Calls operator++(s); s++; // Calls operator++(s, 0); }
The -> operator is different from the other operators. Although you use it as a binary operator, you overload it as a unary operator. It must be implemented as a member function, so the function takes no arguments. It must return one of the following:
An object of class type, for which the type overloads the -> operator
A pointer to a class type
A chain of -> operators is followed until it ends with a pointer to a class type. The actual right operand must name a member of that class. The -> operator is most often overloaded to implement a smart pointer. See the auto_ptr<> template in the <memory> section of Chapter 13 for an example.
The function call operator (operator( )) takes any number of arguments. It must be implemented as a member function. To invoke the operator, use an object of class type as the "name" of the function. Pass the arguments as you would any other function arguments. With a simple variable of class type, the syntax looks like an ordinary function call.
An object that overloads the function call operator is often called a functor. Functors are typically used with the standard algorithms to better encapsulate functionality. Some algorithms, such as for_each, also permit the functor to store state information, which cannot be done with a plain function. Comparison functions for the associative containers are easier to implement as functors. See <algorithm> and <functional> in Chapter 13 for examples.
A new expression (Chapter 3) calls operator new to allocate memory, and a delete expression calls operator delete. (A new[] expression calls operator new[], and a delete[] expression calls operator delete[]. For the sake of simplicity, whenever I refer to a new expression, I mean a new expression or new[] expression. Similarly, operator new refers to operator new and operator new[]. Ditto for delete.)
You can overload operator new and operator delete in the global scope or as members of a class. In the global scope, the functions must not be static, nor can you declare them in a namespace. When you define these operators as member functions, the functions are always static, even if you omit the static specifier. If you do not overload the global operators, the C++ library provides an implementation for you. (See <new> in Chapter 13.) If you do not overload the operators for a class, the global operators are used.
If a class overloads the operator new and operator delete functions, the corresponding operator functions are called for new and delete expressions involving that class. When overloading operator new or operator delete as member functions or with placement arguments, you can call the global operator, as shown in Example 5-25.
#include <cstddef> #include <iostream> #include <memory> #include <new> #include <ostream> class demo { public: static void* operator new(std::size_t size) throw (std::bad_alloc) { std::cout << "demo::new\n"; if (instance == 0) instance = ::new demo; ++count; return instance; } static void operator delete(void* p) { std::cout << "demo::delete\n"; if (--count == 0) { ::delete instance; instance = 0; } } static demo* make( ) { return new demo( ); } private: demo( ) {} demo(const demo&); static demo* instance; static std::size_t count; }; demo* demo::instance; std::size_t demo::count; int main( ) { std::auto_ptr<demo> s1(demo::make( )); std::auto_ptr<demo> s2(demo::make( )); return s1.get( ) == s2.get( ); }
The first parameter to operator new has type size_t and is the amount of memory to allocate. The function returns a pointer to the allocated memory as a void*. Additional parameters are allowed for placement new functions (see Chapter 3). The first parameter to operator delete is a void* pointer to the memory; the function returns void. Additional parameters are allowed for placement delete. See <new> in Chapter 13 for more information about overloaded operator new and operator delete.
When you overload the operator new and operator delete functions, you will probably want to overload the scalar (operator new) and array versions (operator new[]) of the operator. The scalar and array versions often behave identically, but you have the option of making them behave differently. Note that the compiler initializes the objects, so your allocation or deallocation function does not need to know the number of objects being allocated or freed.
If you overload operator new, you should probably also overload operator delete. In the case of placement new, the corresponding placement delete function is called if an exception is thrown while constructing the newly allocated object or array. Without a corresponding placement delete function, no operator delete function is called. This is the only time a placement delete function is called. A delete expression always calls the plain, single-argument form of operator delete.
A class can declare type conversion operators to convert class-type objects to other types. The operator functions must be nonstatic member functions. The name of each operator is the desired type, which can be a series of type specifiers with pointer, reference, and array operators, but cannot be a function or array type:
class bigint { public: operator long( ); // Convert object to type long. operator unsigned long( ); operator const char*( ); // Return a string representation ... };