5.4 Operator Overloading

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.

Table 5-2. Operators and overloading

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 Operators

When 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);

5.4.1 Short-Circuit Operators

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.

5.4.2 Comma Operator

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.

5.4.3 Increment and Decrement

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

Example 5-24. Overloading the increment operator
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);

}

5.4.4 Member Reference

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.

5.4.5 Function Call

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.

5.4.6 operator new and operator delete

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.

Example 5-25. Overloading operator new and operator delete
#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.

5.4.7 Type Conversion

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

  ...

};