Archive

C++ - Part 4



Tony Houghton

This month's article is about overloading, a way of defining groups of functions, or even operators, with the same name, performing similar functions, but on different types of data. C++ obviously cannot enforce that overloaded functions and operators should behave similarly to each other, so overloading, like much of C++, is open to some degree of abuse. Avoid defining new meanings for functions or operators which are not logical and/or similar to existing meanings.

Please note that in the example program fragments, I will sometimes introduce methods (i.e. member functions) with definitions and sometimes with declarations. You should be aware of the difference and know how to provide a declaration to match a definition and vice versa (see Archive 8.12).

The term 'argument' refers to a variable passed to a function as seen by the function being called. 'Parameter' refers to a variable as seen by the expression calling the function. Just to confuse you, an alternative term for argument is 'formal parameter' (as used in compilation error messages).

Function overloading

Overloading functions is as simple as just declaring/defining more than one function happening to have the same name. The rules are that each overloaded function must have different types of arguments from other functions with the same name; you cannot declare functions which differ only in their return type, because the compiler would not know which one to call when called without using its return value:

void sort(int);
void sort(float);
void sort(char);
int sort(int);  // error: sort(int)
                // already declared

Each of these functions must be defined separately. There is not really any more to say about function overloading - it's as simple as that.

Class member function overloading

Overloading member functions is as easy as overloading ordinary functions:

class String {
  // ...
public:
  int find(char);
  int find(char *);
};

Constructors are a slightly special case, it is common to provide a default constructor, either with no arguments or with whatever arguments are needed to construct an object, and a copy constructor. For a class X, a copy constructor is declared as:

  X(X &);  // (within X's definition)

X(X) is not allowed. A copy constructor can be used as part of a solution to the problem (caveat) I described in Archive 8.12. To recap, for a class such as:

class Array {
  int size;
  int *array;
  // ...
public:
  Array(int num_elements);
  ~Array()
  { if (array) delete[] array; }
  // ...
};

the following program will be passed by the compiler, but will crash at run-time:

int main()
{
  Array array1(100);
  Array array2 = array1;
  // ...
}

This is because the default copy operation is a member-wise copy, so array2 will have the same values of size and array as array1. When the objects are deleted, both array1.array and array2.array are deleted. As they both point to the same data, this is a potential crash.

A copy constructor to solve this would be (ignoring error-checking):

Array::Array(Array &original)
  : size(original.size)
{
  array = new int[size];
  if (array)
    memcpy(array, original.array,
           size * sizeof(int));
}

Now each copied object will be given its own distinct copy of the data. The class isn't quite safe yet, copying an Array to an already existing Array will cause the same problem as before. See below for the cure.

Destructors never take arguments, so obviously they cannot be overloaded.

Operator overloading

If you had a class representing a mathematical type such as complex numbers or matrices, it would be convenient to be able to use mathematical operators such as + or * with them, instead of having to use functions called add() and multiply(). C++ allows just that with operator overloading. The operators which can be overloaded are:

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

The last five are not as straightforward as the others, each will be described separately. Operators which cannot be overloaded are:

. .* :: ?: sizeof

# and ## are not operators, but preprocessor directives, so they cannot be overloaded either. It is not possible to add completely new operators or change the number of operands needed by an operator (e.g. you cannot define a unary /). Operators which can be either unary or binary, such as -, can be overloaded as either.

Operators cannot be redefined to have new meanings for existing types or for pointers; an overloaded operator must take at least one class or reference to class argument, or be a member function of a class.

= [] () ->

can only be overloaded as non-static class members, because their first operands need to be lvalues.

= & ,

already have meaning when applied to classes, so as an alternative to overloading them, it is possible to declare them as private members (no definition is needed) to restrict unintentional use.

Operators need not have the same relationships with each other, e.g. a+=b meaning the same as a=a+b. However, you are strongly advised to keep these relationships where appropriate.

Operator function notation

An operator function is represented by a function with the name operator followed by the operator's symbol. For unary operators (e.g. !), the object it acts on is the operator function's only argument, for binary operators (e.g. /) the object before the operator is the function's first argument, and the object after the operator its second. In either case the result of the operation is given by the function return value. To illustrate this with an incomplete class for complex numbers:

struct Complex {
  float re, im;
};
Complex operator+(Complex a,
                  const Complex &b)
// a is local, so no need to define
// temporary variable for result.
// b is reference for efficiency;
// a temporary need not be created
// if it is passed an actual Complex
{
  a.re += b.re;
  a.im += b.im;
  return a;
}
Complex operator-(Complex a)
// One argument, so unary minus
{
  a.re = -a.re;
  return a;
}

These operator functions can now be called with either notation:

Complex c = a + b;
Complex c = operator+(a, b);

Both are equivalent to each other; the second form is rather rare: why make a function an operator if you're not going to use it as one?

Operators as class members

An alternative to stand-alone operator functions is to make them class members. In this case the first operand for binary operators, or the only operand for unary operators, is replaced by the object the operator is called for. The Complex example becomes:

class Complex {
  float re, im;
   // Much safer as private
public:
  Complex(float r, float i = 0)
    : re(r), im(i) {}
  // Copy constructor unnecessary,
  // default member-wise copy is OK
  Complex &assign(float r, float i)
  // Needed because member operator=
  // can only take one argument
  {
    re = r;
    im = i;
    return *this;
  }
  Complex operator+(Complex b)
  // This time b is not reference to
  // allow local copy to be modified.
  // a is replaced by 'this' object
  {
    b.re += re;
    b.im += im;
    return b;
  }
  Complex operator-()
  {
    // Temporary is needed, because
    // - should not actually alter
    // 'this' object
    Complex b(-re);
    b.im = im;
    return b;
  }
};

When deciding whether to define an operator as a stand-alone function or a class member, the rule of thumb is to always use a member unless you have a good reason not to do so. One exemption would be an operator which acts on two classes, being associated to neither class more than another. Additionally, the compiler can make no assumptions as to whether an operator is associative, so if you had a class X with X X::operator+(int) you could type x+1 but if, for some strange reason, you absolutely needed to be able to reverse the order of operands and type something like 1+x you would need:

inline X operator+(int a, X x)
{ return x+a; }

which is not a member.

User-defined type conversion

Complex's constructor gives us a way of creating a Complex from any type which can be implicitly converted to float. Furthermore, if you attempt to assign a float (or other arithmetic type) to an already existing Complex, the compiler will create a temporary Complex from the float, before assigning it to the existing Complex. The following mini-program is therefore correct:

int main()
{
  Complex a = 1;
    // 1 converted to float (at
    // compilation, not at run-time),
    // float passed to Complex
    // constructor
  Complex b = a;
    // Standard copy-construction
  a = b;
    // Standard member-wise copy
    // operation
  a = 2;
    // 2 converted to float, passed
    // to Complex constructor to
    // create temporary which is then
    // copied to a
}

If run-time efficiency is more important than simplicity of class design, you can avoid the creation of a temporary Complex during assignment by a float etc by overloading = :

  // (within Complex definition)
  Complex &operator=(float r)
  { return assign(r, 0); }

Going back to the bugged Array class, overloading = completes the protection against repeated deletion:

Array &Array::operator=(Array &a)
{
  delete[] array; 
  array = 0;
  size = a.size;
  if (a.array)
  {
    array = new int[size];
    memcpy(array, a.array,
           size * sizeof(int));
  }
}

In some circumstances, you may wish to convert a class to a simpler type. This can be done by using one of the type conversion operators, which are defined as operator <type name>(). No return type is declared - this is implicitly the type name used as the operator. As an unlikely example, you might want to simplify the notation for taking the magnitude of a Complex by defining this as a conversion from Complex to float:

  // (within Complex definition)
  operator float()
  { return sqrt(re*re + im*im); }
int main()
{
  Complex a(/*...*/);
  float f = a;
    // f takes magnitude of a
}

To convert a class to a pointer, you would have to typedef a new name for the pointer; * is not allowed as part of the type name of a conversion operator.

It is not advisable to use this sort of type conversion unless it is specifically useful. Such a case would be some sort of container class whose main purpose is to hold an object of another type.


Contents - The Archives - Archive Articles