Guidelines to Overloading Operators

 

The following sections summarize the most important guidelines to follow when overloading operators. Remember that our guidelines are useful to maintain consistency with the built-in definitions of operators. Maintaining consistency is essential for transparent use. Clients should be able to use all of the operators described in this chapter with user defined types in the same way in which they are accustomed.

We begin by reviewing which operators we recommend overloading as members or as non-members. Then, we summarize when to overload one or more operators. We conclude by walking through a step-by-step approach to overloading operators. Even though much of this material has already been covered in previous sections, we have pulled all of our guidelines together to enable quick reference. Remember, these sections focus on what should be done with operator overloading and not what can be done. They are simply guidelines rather than requirements.

 

13.8.1  Overloading Operators as Members or Non-Members

Choosing whether to overload operators as members or non-members is dependent on the data type of the first operand and whether or not symmetry must be taken into account. There are three categories of operators that we recommend be defined as members: those that must be members, unary operators where the only operand is required to be an object, and binary operators that make the most sense with the first operand being an object. Notice that all unary operators should be members, since all unary operators require that the one and only operand be an object of a class when overloading. Table 13-13 summarizes these recommendations.

 

Operators that must be overloaded as members:

=                                        simple assignment operator

[]                                     subscript operator

()                                     function call operator

->                                     indirect member access operator

->*                                   indirect pointer to member access operator

 

All unary operators make sense as members:

&                                        address of operator

*                dereference operator

++          increment operator (both prefix and postfix)

--          decrement operator (both prefix and postfix)

+                                        plus operator

-                                        minus operator

~                bitwise negation operator (complement)

!                logical negation operator

 

And, these binary operators should be members:

*= /= %= += -=   arithmetic assignment operators

<<= >>= &= ^= |= bitwise assignment operators

 

Table 13-13: Guidelines ­– Operators that should be Overloaded as Members

 

Operators that allow either the first or second operand to be an object of a class are best defined as non-member functions. All of the arithmetic, bitwise, relational, and equality binary operators fall into this category. For each of these operators, if both operands are not of the same type, then we should make sure that either type can be placed as either the first or second operand. This means that we typically overload more than one version of these operators to handle all possible combinations. Table 13-14 summarizes those operators that we recommend be defined as non-members.

 

 

* / % + -        arithmetic operators

<< >> & ^ |      bitwise operators

< <= > >=        relational operators

== !=            equality operators

*= /= %= += -=   arithmetic assignment operators

 <<= >>= &= ^= |= bitwise assignment operators

 

Table 13-14: Guidelines – Operators that should be Overloaded as Non-Members

 

There are three other operators that can be overloaded but that are not listed in the previous two tables. These are the logical and comma operators. We do not recommend overloading these operators as there is no way for us to overload them in a way that is consistent with their built-in meaning. These recommendations are summarized in Table 13-15.

 

&& ||            logical operators

,                comma operator

 

Table 13-15: Guidelines – Operators that should not be Overloaded

 

13.8.2 What Operators to Consider Overloading

We are now ready to review some general recommendations that when followed will assist in creating user defined types that behave as clients might expect. As we design our classes, we should consider each of the following suggestions. However, there may be times when some, or all, of these suggestions will not be appropriate. Therefore, take them into consideration, but don't follow them blindly!  Table 13-16 summarizes these general recommendations and Table 13-17 shows which operators work best in pairs. Consider using these tables as a quick reference when double checking if all of the necessary operators have been overloaded for a new user defined type.

To obtain behavior consistent with built-in types, we should begin by evaluating whether or not both the extraction and insertion operators are needed. This will allow us, at a minimum, to read and write objects of that type.

For classes that dynamically allocate memory on an object-by-object basis, we should overload the copy constructor and assignment operator to perform a deep copy. Therefore, it is common for classes to have at a minimum a copy constructor, an assignment operator, and the extraction and insertion operators overloaded.

Whenever we overload any of the arithmetic or bitwise binary operators, we should also consider overloading the corresponding arithmetic or bitwise compound assignment operators. And, when we overload any operator that has a counterpart operator (++ and --, + and -, << and >>, * and /, etc.) then we should overload neither or both. Only when the occasion requires will we overload one but not the other (e.g., it would be strange to be able to use ++ to increment but not -- to decrement).

Whenever we overload any of the equality or relational operators, we recommend overloading all of relational operators. This means that if we can compare for equality, we should be able to compare for inequality.

 

To be able to read and write objects, overload:

>> <<            as extraction and insertion operations

 

If dynamic memory is part of the object, overload:

=                                        simple assignment operator

 

Provide compound assignment operators if overload any of:

* / % + -        arithmetic operators

<< >> & ^ |      bitwise operators

 

Provide all relational operators if overload any:

< <= > >=        relational operators

 

Provide all equality operators if overload any:

== !=            equality operators

*= /= %= += -=   arithmetic assignment operators

 

Table 13-16: Guidelines ­– What Operators to Consider Overloading

 

Provide both forms as pairs:

-> ->*                           indirect member and pointer to member

                                            access operators

& *              address of and dereference operators

++ --            increment and decrement operators

                                            (both prefix and postfix)

+ -                                   plus and minus operators

* /              multiplication and division operators

+ -              addition and subtraction operators

new delete                 dynamic allocation/deallocation

new[] delete[]       dynamic array allocation/deallocation

 

Table 13-17: Guidelines ­– Operators that Work Best in Pairs

 

13.8.3  Operators That Modify the First Operand

To maintain consistency with the built-in definitions, only the following operators should be designed to modify the contents of the current object:  =, +=, *=, /=, %=, -=, ++, --, [], and * (dereference). These should be implemented as class members where the first operand is an object of that class. This is preferred over friend functions, so that the object is sent implicitly and the number of friends can be minimized. All other operators implemented as members should be specified as constant members. This will allow us to use those operators with constant objects as the first operand.

 

13.8.4  Using Temporaries

When overloading operators, we have the ability to return objects as rvalues, lvalues, or modifiable lvalues. By returning an object by value, the information returned can only be used as an rvalue. By returning an object by reference, the information returned can be used as an rvalue, lvalue, or modifiable lvalue. Operators that use temporary objects instead of modifying the current object only can return those objects by value. This is because objects local to a function have a lifetime from their definition until the end of the function. Returning a reference to a temporary isn't possible. This is because return by reference requires that the returned object's lifetime correspond to the client's expectations (and be within the client's control). This means that we must never return a reference to local object.

 

13.8.5  Operators That Allow for Constant Operands

To maintain consistency with the built-in definitions, make sure to consider when it is appropriate to allow the operands to be constant or non-constant objects. Operators that do not modify the first operand should either be constant members or friend functions that specify the first argument by value or as a constant reference. Binary operators that do not modify the second operand should specify the corresponding argument either by value or as a constant reference. Use pass by value when working with built-in types and constant references when working with user defined types that perform deep copies.

 

13.8.6  Conventions

Table 13-18 summarizes the behavior of each of the built-in operators. This table should be used to assist in overloading operators for user defined types in order to keep the usage in line with common practice.

 

Table 13-18: Guidelines ­– Conventions

 

13.8.7  A Summary of the Guidelines

The following summarizes the guidelines discussed in this chapter when overloading an operator. This represents a step-by-step approach to designing our overloaded operators. Remember, it takes careful planning to meet the objectives of operator overloading.

 

1)   Determine which operators should be overloaded to ensure that the user defined data type will behave consistently with the fundamental data types. Refer to Tables 3-16 and 3-17 for a check list.

 

2)   For each operator, determine the data types for the operands. When the first operand can be an object of the target class, then it should be overloaded as a member. Otherwise, it should be overloaded as a non-member friend.

 

3)   For each operator, determine whether or not the first operand can be modified. If the first operand cannot be modified for an overloaded member, then the member should be constant; for a non-member, then it should be specified either by value or as a constant reference.

 

4)   For each operator, determine whether or not the second operand can be modified. If the second operand cannot be modified, it should either be by value or a constant reference.

 

5)   For each operator, determine the data type of the residual value.

 

6)   For each operator, determine whether the residual value should be an rvalue, a non-modifiable lvalue or a modifiable lvalue. If it is an rvalue, then the operator can either return by value or by const reference. If it is an lvalue, then the operator must return by reference.