Operators

What exactly is an operator? We've used some before; the “assignment operator," the “insertion operator," etc. An operator is basically a certain character (or two) that C++ uses to describe an operation on one or more pieces of data. For example, some of the operators in C++ are:

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

Most of these operators you’ve seen before: “+” will add two values, “=” will assign a value to a variable, and “>” will test if one value is greater than another. Many of these operators have already been defined for many types. However, if you were to try to use one of these operators with an ADT of your own, you can't: you’ll get compiler errors. Operator overloading allows you to implement these operators with your own types.

Technically, everything you can do with operator overloading you can also do with member functions, but using operators will very often make your code cleaner, easier to understand, and easier to create.

Operator Overloading

So, how does one go about overloading an operator? It’s straightforward: an operator overload is prototyped in almost the same way a member function is.

<return type> operator<operator>(<parameters>);

...for example...

bool operator==(const type& comp);

Calling an overloaded operator is even simpler than calling a member function—simply use the operator. When an operator regarding two objects is invoked, the object left of the operator is the calling object. Hence, that object's operator function will be called. The object on the right of the operator will be passed as a parameter. This rule can be bent a bit; see friend operators.

Hence...

result = obj1 == obj2;

...is equivalent to...

result = obj1.operator==(obj2);

Implementing an overloaded operator is also not much different than a member function, for example a simple comparison operator...

bool type::operator==(const type& comp) {
	return x == comp.x && y = comp.y;
}

This example tests if the data members “x” and “y” are the same in both the calling object and the comparison parameter. You can apply this same logic to most of the other operators. For example, a “+” operator...

class type {
	type(int _x, int _y);

	type operator+(const type& add);
	
	int x, y;
}

type type::operator+(const type& add) {
	return type(x + add.x, y + add.y);
}

This example returns a new object of type “type” using the combined “x” and “y” values of the calling object and parameter. Note that this uses a parametrized constructor to create the object to return.

Return Types & Chaining

Technically, return types for operators are no different than for other functions. However, overloaded operators almost always return a reference to or a copy of the type it applies to. For example, the assignment operator is almost always prototyped as...

type& operator=(const type& src);

The operator returns by reference because it is returning an object that already exists—the calling object. Do this when possible, as it is more efficient than returning by value (and hence calling the copy constructor). You don't need to worry about other parts of your program accessing the reference, as the only way to capture the value is to copy it elsewhere.

The reason we don’t just return “void” is for chaining—using multiple operators in a row, as in...

obj1 = obj2 = obj3;

Because the assignment operator is right-associative, this statement is equivalent to...

obj1.operator=(obj2.operator=(obj3));

Here, you can see why the assignment operator has to return the same type as the object it’s using: the inner assignment returns another object of the same type so that obj1's assignment operator can then use that. This works the same way with operators such as “+,” “-,” “++,” and many others.

In operators such as “+” and “-,” you don't want to modify either instance, but you need a way to return a result. Simply create and return a new instance representing the combined values.
However, in operators that modify the calling object (the assignment operator, “+=”, etc.), you want to return the calling object. How do you access the calling object? The keyword “this” gives you a pointer to it. However, you need to return the actual object—not its address. Therefore, dereference “this” to return the calling object.

type& type::operator=(const type& src) {
	x = src.x;
	return *this;
}

Friend vs. Member Operators

So far, you've only seen operators overloaded as member functions. You can also implement operators as free functions, and can use them with classes through friendship. The syntax of a friend operator is very similar to that of a member, except that because the operator is free function, there is no calling object. Hence, you must pass both objects as parameters.

class type {
public:
	type(int _x, int _y);

private:
	int x, y;

	friend type operator+(const type& left, const type& right);
};

The only difference in implementation is that there is no calling object.

type operator+(const type& left, const type& right) {
	return type(left.x + right.x, left.y + right.y);
}

It may seem like there is no point in making an operator a friend over a member—this is true for most operators. However, there are a few special cases. First, the assignment operator must always be a member. Second, the insertion and extraction operators must always be friends. Any other operators that should be a member of an inaccessible class must be friends. More on that in a bit.

Assignment Operator vs. Copy Constructor

You might think that the assignment operator and the copy constructor do much the same thing, and you’d be right: they both copy data from one instance of a type into another. However, there are a couple of key differences. First, you should remember that the constructor of an object is only called once, when the object is instantiated. This means that the copy constructor will always start with a blank slate, and never needs to clean up before copying in new data. The assignment operator, on the other hand, can be called any number of times and consequently must make sure to reset the instance before copying in new data.

Furthermore, because the copy constructor is only called when you’re creating a new object, you know that your parameter, the source object, cannot be the same object as the calling object. On the other hand, with the assignment operator, you have no such guarantee. This may sound confusing, but look at the code...

type object1(object1);

This makes no sense, and will not compile.

object2 = object2;

This is totally valid and will compile.

type object1;
type* objPtr;
// many lines of code later
objPtr = &object1;
// many lines of code later
object1 = *objPtr;

This is the sort of situation where this bug can actually occur.

Within your assignment operator, you should always make sure the source object is not the same as the calling object. Of course, the only way to know for sure if two things are exactly the same is to compare their addresses. The keyword “this” conveniently provides us with a pointer to the current calling object. Note that to make this test, you must pass the source parameter by reference—else a copy is be made when passed to the function.

type& type::operator=(const type& source) {
	if(this != &source)
	{
		// source and calling objects are different, do copy
	}
	// source and calling objects are the same, do nothing
}

Finally, although counterintuitive, if you use the assignment operator when instantiating an instance, your program will actually call the copy constructor. The assignment operator is only called when you have two objects, both already instantiated.

type object2 = object1;

This will call the copy constructor

type object2;
object2 = object1;

This will call the assignment operator

Insertion & Extraction Operators

You can overload the insertion and extraction operators for your own class. This greatly streamlines input and output.

cout << object1;

Earlier, I said that the left side is the calling object—so would that make “cout” the calling object? Actually, yes. However, we can’t modify “cout,” so we need another way to implement the operator. Hence, it must be a friend rather than a member of our class.

The insertion operator should take a parameter of type “ostream&,” as this is what describes an output stream. "cout" and file out variables are both "ostreams," and you can define your own. The operator should also return the ostream, for chaining. Note that this should be a return by reference, as the next operator in the chain needs the actual same output stream, not a copy.

On the other hand, the extraction operator should take a parameter of type “istream&,” as this is what describes an input stream. "cin" and file input variables are both "istreams," and you can define your own. Again, the operator should return the same type (by reference) for chaining.

Implementation is quite simple—in the operator, simply use the ostream variable as you would "cout," formatting and outputting each value individually. Then, remember to return the ostream when you are done. Finally, because these operators are always a friend of your class (and do not have a calling object), you must pass the instance to output as a parameter. This should be a constant reference.

For example...

ostream& operator<<(ostream& out, const type& source) {
	out << source.x << “ “ << source.y << endl;
	return out;
}

Is called by...

type object1;
cout << object1;

And can be called by...

ofstream fout("out.txt");
type object1;
fout << object1;

In this case, "fout" is passed as the "out" ostream.

Brackets & Parenthesis Operators

Finally, a quick word on the bracket/parenthesis operators: they are usually used to look up a piece of data within a data structure, and may take any parameters you desire.

class type {
public:
	char operator[](int index);
	char operator()(int index);
	// ...

private:
	// ...
};

char type::operator[](int index) {
	return data[index];
}

char type::operator()(int index) {
	return data[index];
}

int main() {
	type object1;

	char val = object1[10];
};

The final line calls the square brackets operator from type with the parameter 10. You get the idea.

Programming Exercises

All the data members in these classes should be private! Member functions should mostly be public.

  1. Create your own “string” class that uses a dynamically allocated c-style string at its core. It should implement at least the following operators:
  2. = - copy strings
    + - concatenate strings
    [] – get character at an index
    == - test if strings are equal
    << - output string
    >> - input string
  3. If you’d like to continue doing card-related exercises, try creating a “card” class which implements overloaded operators.