Topic : Operator Overloading
Author : Danny Kalev
Page : 1 Next >>
Go to page :


Introduction
A built-in operator can be extended to support user-defined types as well. Such an extension overloads the predefined meaning of an operator rather than overrides it. Although ordinary functions can offer the same functionality, operator overloading provides a uniform notational convention that is clearer than the ordinary function call syntax. For example

Monday < Tuesday; //overloaded <
Greater_than(Monday, Tuesday);


The history of operator overloading can be traced back to the early days of Fortran. Fortran, the first high-level programming language, presented the concept of operator overloading in a way that was revolutionary back in the mid-1950s. For the first time, built-in operators such as + or - could be applied to various data types: integers, real and complex. Until then, assembly languages -- which didn't even support operator notation -- had been the only choice for programmers. Fortran's operator overloading was limited to a fixed set of built-in data types; they could not be extended by the programmer. Object-based programming languages offered user-defined overloaded operators. In such languages, it is possible to associate a set of operators with a user-defined type. Object-oriented languages usually incorporate operator overloading as well.

The capability to redefine the meaning of a built-in operator in C++ was a source of criticism. People -- mostly C programmers making the migration to C++ -- felt that overloading an operator was as dangerous as enabling the programmer to add, remove, or change keywords of the language. Still, notwithstanding the potential Tower of Babel that might arise as a result, operator overloading is one of the most fundamental features of C++ and is mandatory for generic programming (generic programming is discussed in Chapter 10, "STL and Generic Programming."). Today, even languages that tried to make do without operator overloading are in the process of adding this feature.

This chapter explores the benefits as well as the potential problems of operator overloading. It also discusses the few restrictions that apply to operator overloading. Finally, it presents conversion operators, which are a special form of overloaded operators.

An overloaded operator is essentially a function whose name is an operator preceded by the keyword operator. For example

class Book
{
private:
  long ISBN;
public:
//...
  long get_ISBN() const { return ISBN;}
};
bool operator < (const Book& b1, const Book& b2) // overload operator <
{
  return b1.get_ISBN() < b2.get_ISBN();
}


Operator Overloading Rules of Thumb
C++ enforces few restrictions on operator overloading. For instance, it does not prohibit a programmer from overloading the operator ++ in order to perform a decrement operation on its object (to the dismay of its users, who would instead expect operator ++ to perform an increment operation). Such misuses and puns can lead to a cryptic coding style that is almost unintelligible. Often, the source code that contains the definition of an overloaded operator is not accessible to its users; therefore, overloading an operator in an unexpected, nonintuitive manner is not recommended.

The other extreme, avoiding operator overloading altogether, is not a practical choice either because it means giving up an important tool for data abstraction and generic programming. When you are overloading an operator to support a user-defined type, therefore, it is recommended that you adhere to the basic semantics of the corresponding built-in operator. In other words, an overloaded operator has the same side effects on its operands and manifests the same interface as does the corresponding built-in operator.

Members and Nonmembers
Most of the overloaded operators can be declared either as nonstatic class members or as nonmember functions. In the following example, the operator == is overloaded as a nonstatic class member:

class Date
{
private:
  int day;
  int month;
  int year;
public:
  bool operator == (const Date & d ); // 1: member function
};


Alternatively, it can be declared as a friend function (the criteria for choosing between a member and a friend will be discussed later in this chapter):

bool operator ==( const Date & d1, const Date& d2); // 2: nonmember function
class Date
{
private:
  int day;
  int month;
  int year;
public:
  friend bool operator ==( const Date & d1, const Date& d2);
};


Nonetheless, the operators [], (), =, and -> can only be declared as nonstatic member functions; this ensures that their first operand is an lvalue.

Operator's Interface
When you overload an operator, adhere to the interface of its built-in counterpart. The interface of an operator consists of the number of operands to which it applies, whether any of these operands can be altered by the operator, and the result that is returned by the operator. For example, consider operator ==. Its built-in version can be applied to a wide variety of fundamental types, including int, bool, float, and char, and to pointers. The underlying computation process that is required for testing whether the operands of operator == are equal is an implementation-detail. However, it can be generalized that the built-in == operator tests its left and right operands for equality and returns a bool value as its result. It is important to note also that operator == does not modify any of its operands; in addition, the order of the operands is immaterial in the case of operator ==. An overloaded operator == should conform to this behavior, too.

Operator Associativity
Operator == is binary and symmetrical. An overloaded version of == conforms to these qualities. It takes two operands, which are of the same type. Indeed, one can use operator == to test the equality of two operands of distinct fundamental types, for example char and int. However, C++ automatically applies integral promotion to the operands in this case; as a result, the seemingly distinct types are promoted to a single common type before they are compared. The symmetrical quality implies that an overloaded operator == is to be defined as a friend function rather than a member function. So that you can see why, here's a comparison of two different versions of the same overloaded operator ==:

class Date
{
private:
  int day;
  int month;
  int year;
public:
  Date();
  bool  operator == (const Date & d) const;  // 1 asymmetrical
  friend bool operator ==(const Date& d1, const Date& d2); //2 symmetrical
};
bool operator ==(const Date& d1, const Date& d2);


The overloaded operator == that is declared as a member function in (1) is inconsistent with the built-in operator == because it takes two arguments of different types. The compiler transforms the member operator == into the following:

bool  Date::operator == (const Date *const,  const Date&) const;

The first argument is a const this pointer that points to a const object (remember that this is always a const pointer; it points to a const object when the member function is also const). The second argument is a reference to const Date. Clearly, these are two distinct types for which no standard conversion exists. On the other hand, the friend version takes two arguments of the same type. There are practical implications to favoring the friend version over the member function. STL algorithms rely on a symmetrical version of the overloaded operator ==. For example, containers that store objects that do not have symmetrical operator == cannot be sorted.

Another example, built-in operator +=, which also takes two operands, modifies its left operand but leaves the right operand unchanged. The interface of an overloaded += needs to reflect the fact that it modifies its object but not its right operand. This is reflected by the declaration of the function parameter as const, whereas the function itself is a non-const member function. For example

class Date
{
private:
  int day;
  int month;
  int year;
public:
  Date();
  //built-in += changes its left operand but not its right one
  //the same behavior is maintained here
  Date & operator += (const Date & d);
};


To conclude, it can be said that every overloaded operator must implement an interface that is similar to the one that is manifested by the built-in operator. The implementer is free to define the underlying operation and hide its details -- as long as it conforms to the interface.

Restrictions on Operator Overloading
As was previously noted, an overloaded operator is a function that is declared with the operator keyword, immediately followed by an operator id. An operator id can be one of the following:

new  delete    new[]     delete[]
+    -    *    /    %    ^    &    |    ~
!    =    <    >    +=   -=   *=   /=   %=
^=   &=   |=   <<   >>   >>=  <<=  ==   !=
<=   >=   &&   ||   ++   --   ,    ->*  ->
()   []

In addition, the following

Page : 1 Next >>