Operator Functions and Overloading

Operator functions permit you to provide operators for your classes such as the +=, ++, + and = operators. Operator Overloading is similar to function overloading, and you can specify how an operator for your class works with several other data types. You can not overload the ternary ? operator, nor can you make up operators (such as $).

The basic format for an overloaded operator is:

	type classname::operator#( arglist )
	{
	}

type refers to the value of the operator, and is normally the same object type.
classname is the classname
operator is required
The # is replaced with the operator to be overloaded (such as +, -, *, etc).
The arglist contains the data types this operator works with.

class Money 
{
public:
	Money( long pDollars=0, unsigned char pCents=0);
	Money operator+( const Money& M );
private:
	long Dollars;
	unsigned char Cents;
};
Money::Money( long pDollars, unsigned char pCents)
{
	Dollars=pDollars;
	Cents=pCents;
}
Money Money::operator+( const Money& M )
{
	Money Tmp;
	Tmp.Dollars+=M.Dollars;
	Tmp.Cents+=M.Cents;
	return( Tmp );
}
void main()
{
	Money A(100,0), B(10,0);
	A = A + B;
}

 

Note that issues concerning the copy constructor still exist for overloaded operator functions. This example works because this class is very simple, and the default assignment/copy for the class will not cause any problems, so we don't need to write a copy constructor here.

Binary Operators

A binary operator is one that takes two arguments. The operator may be written either as a non-static member function, or as a stand-alone function outside a class. As a stand-alone function (non-member), it is subject to the same member access rules as any other class, unless the class declares the function as a friend.

For example, if you were to write the + operator for the Money class above, it could be done two different ways:

// this is the implied 'A' parameter.  Member function:
Money operator+( const Money&B ); 
// A stand-alone function:
Money operator+( const Money&A, const Money& B ); 

The object to the left of the operator in use is the one that dictates which one to invoke.

For example, if you had a String and a Money class, they could be defined as:

String String::operator+( const Money& B );
Money Money::operator+( const String& B );

Now, you would be safe to add:
String S, SRes;
Money M, MRes;

SRes = S + M; // would invoke the String::operator+ function
MRes = M + S; // would invoke the Money::operator+ function

In the case of a non-member function the order of the parameters dictates which operator function is called. For example, if the Money::operator+ above were written as a non-member function, then it would appear with 2 parameters:

Money operator( const Money& A, const String& B );

Now, the M+S code would still compile, and would invoke the non-member function. Note: You can not have member and non-member functions simultaneously for the same overloaded function. This would be an ambiguous definition.

Operators still follow order of precedence, so given an example of:

A = B + C * D;

The operator* function would be called first, and then the operator+ function would be called (for whatever data types B, C, and D might be.

Unary Operators

Unary operators are ones that have one operand. As a member function, the parameter list would be blank (except in the case of the increment and decrement postfix operators).

Examples of unary operators are (as Money member functions):

Money operator++() 
{ if( Cents==99 ) {Cents=0; Dollars++;} else Cents++; return( *this );}
Money operator++(int) 
{ Money Tmp=*this; if( Cents==99 ) {Cents=0; Dollars++;} else Cents++; return Tmp; }

As non-member functions, the above would be:

Money operator++( Money& Dest );
Money operator++( Money& Dest, int );

Conversion Operators

A conversion operator is one that is provided to convert a class to another data type. The conversion operator is invoked automatically when you typecast a class, or if you pass the object to a function that requires a different data type (in which case the compiler automatically attempts a typecast).

A conversion operator is written to return no type (not even void), uses the operator keyword followed by the desired data type, and empty parens. An example for the Money class, to convert it to a long, would be (as a member function):

Money::operator long() 
{ if( Cents ) throw "bad conversion Money->Long"; return( Dollars ); }

Now, we could use the Money type, anywhere a long was needed. For example:

void Foo( long X );
Money M;
long L;
L = M; // Invokes the Money::operator long() function;
Foo( M ); // Invokes the Money::operator long() function;

This behavior is similar to the constructor being called to create a temporary object, and the implicit keyword used to stop this behavior. They both provide a level of automatic type conversion.

Note: Constructors also provide a form of default data conversion. For example, if a date class had no operater= functions, yet the following code we done:

MDate X;
X = 5; // 5 is an integer

Then the above would invoke the constructor for MDate that takes an integer, if it has one. You can imagine the above code as the attempt:

MGDate X;
X = MGDate(5);

If both a constructor that takes an integer and an assignment operator that accepts an integer are available it is not a problem, the assignment operator has precedence.

Daisy-chained Operators

Unlike constructors and destructors, operators are not daisy-chained automatically. This means that if you have a base class called Base, and derive from it a Derived class, the operator= (for example) should be a virtual function. Note: Copy contructors are also not automatically daisy-chained, nor are any of the data members (you will need to assign each member yourself).

The operator= function in the base class does not automatically invoke the operator= in the base class. It is your responsibility to do this, similar to:

Base& Derived::operator=( const Base& Src )
{
	Base::operator=( Src ); // Invoke base classes operator=
	// Now, do what you need.
}

This behavior is different from constructors and destructors with base and derived classes. Before the constructor of a derived class is called, the constructor for it's base is called. After a destructor for a derived class, the destructor for its base is called. This all happens automatically, and also applies for copy constructors.

Dereference operators

You can also write de-reference operators for a class, using the following:

ClassX* operator->(); 
ClassX& operator*(); 
Element operator[]( int Index );

The first form, -> is to make the class mimic pointer behavior. The second is a typical index de-reference. It is important to note that the operator-> is intended to simulate a pointer, but the object that invokes it must be an object, and not a pointer to an object:

SomeClass X; // Note: The '*' or pointer declaration should *not* be here
X->SomeMember = 0;

Using the operator[] is a very common way of implementing safe arrays, where boundary checking takes place. It can also be used to implement multi-dimensional arrays, by returning another class or another pointer.