Overloading Arithmetic and Assignment Operators for Intuitive Class Behavior

Problem

You have a class for which some of C++'s unary or binary operators make sense, and you want users of your class to be able to use them when working with objects of your class. For example, if you have a class named Balance that contains, essentially, a floating-point value (i.e., an account balance), it would be convenient if you could use Balance objects with some standard C++ operators, like this:

Balance checking(50.0), savings(100.0);

checking += 12.0;
Balance total = checking + savings;

 

Solution

Overload the operators you want to use as member functions and standalone functions to allow arguments of various types for which the given operator makes sense, as in Example 8-15.

Example 8-15. Overloading unary and binary operators

#include 

using namespace std;

class Balance {
 // These have to see private data
 friend const Balance operator+(const Balance& lhs, const Balance& rhs);
 friend const Balance operator+(double lhs, const Balance& rhs);
 friend const Balance operator+(const Balance& lhs, double rhs);

public:
 Balance( ) : val_(0.0) {}
 Balance(double val) : val_(val) {}
 ~Balance( ) {}

 // Unary operators
 Balance& operator+=(const Balance& other) {
 val_ += other.val_;
 return(*this);
 }
 Balance& operator+=(double other) {
 val_ += other;
 return(*this);
 }
 
 double getVal( ) const {return(val_);}

private:
 double val_;
};

// Binary operators
const Balance operator+(const Balance& lhs, const Balance& rhs) {
 Balance tmp(lhs.val_ + rhs.val_);
 return(tmp);
}

const Balance operator+(double lhs, const Balance& rhs) {
 Balance tmp(lhs + rhs.val_);
 return(tmp);
}

const Balance operator+(const Balance& lhs, double rhs) {
 Balance tmp(lhs.val_ + rhs);
 return(tmp);
}

int main( ) {

 Balance checking(500.00), savings(23.91);

 checking += 50;
 Balance total = checking + savings;

 cout << "Checking balance: " << checking.getVal( ) << '
';
 cout << "Total balance: " << total.getVal( ) << '
';
}

 

Discussion

The most common case for operator overloading is assignment and arithmetic. There are all sorts of classes for which arithmetic and assignment operators (addition, multiplication, modulo, left/right bit shift) make sense, whether you are using them for math or something else. Example 8-15 shows the fundamental techniques for overloading these operators.

Let's start with what is probably the most common operator to be overloaded, the assignment operator. The assignment operator is what's used when you assign one object to another, as in the following statement:

Balance x(0), y(32);
x = y;

The second line is a shorthand way of calling Balance::operator=(y). The assignment operator is different than most other operators because a default version is created for you by the compiler if you don't supply one. The default version simply copies each member from the target object to the current object, which, of course, is not always what you want, so you can override it to provide your own behavior, or overload it to allow assignment of types other than the current type.

For the Balance class in Example 8-15, you might define the assignment operator like this:

Balance& operator=(const Balance& other) {
 val_ = other.val_;
 return(*this);
}

The first thing that may jump out at you, if you're not familiar with operator overloading, is the operator= syntax. This is the way all operators are declared; you can think of each operator as a function named operator[symbol], where the symbol is the operator you are overloading. The only difference between operators and ordinary functions is the calling syntax. In fact, you can call operators using this syntax if you feel like doing a lot of extra typing and having ugly code:

x.operator=(y); // Same thing as x = y, but uglier

The operation of my assignment operator implementation is simple. It updates the val_ member on the current object with the value from the other argument, and then returns a reference to the current object. Assignment operators return the current object as a reference so that callers can use assignment in expressions:

Balance x, y, z;
// ...
x = (y = z);

This way, the return value from (y = z) is the modified object y, which is then passed to the assignment operator for the object x. This is not as common with assignment as it is with arithmetic, but you should return a reference to the current object just to stick with convention (I discuss the issue as it relates to arithmetic operators shortly).

Simple assignment is only the beginning though; most likely you will want to use the other arithmetic operators to define more interesting behavior. Table 8-1 lists all of the arithmetic and assignment operators.

Table 8-1. Arithmetic and assignment operators

Operator

Behavior

=

Assignment (must be member function)

++=

Addition

--=

Subtraction

**=

Multiplication or dereferencing

//=

Division

%%=

Modulo

++

Increment

--

Decrement

^^=

Bitwise exclusive or

~

Bitwise complement

&&=

Bitwise and

||=

Bitwise or

<<<<=

Left shift

>>>>=

Right shift

For most of the operators in Table 8-1 there are two tokens: the first is the version of the operator that is used in the conventional manner, e.g., 1 + 2, and the second is the version that also assigns the result of an operation to a variable, e.g., x += 5. Note that the increment and decrement operators ++ and -- are covered in Recipe Recipe 8.13.

Implementing each of the arithmetic or assignment operators is pretty much the same, with the exception of the assignment operator, which can't be a standalone function (i.e., it has to be a member function).

The addition operator is a popular choice for overloading, especially since it can be used in contexts other than math, such as appending one string to another, so let's consider the addition assignment operator first. It adds the righthand argument to the lefthand argument and assigns the resulting value to the lefthand argument, as in the statements:

int i = 0;
i += 5;

After the second line has executed, the int i is modified by having 5 added to it. Similarly, if you look at Example 8-15, you would expect the same behavior from these lines:

Balance checking(500.00), savings(23.91);
checking += 50;

That is, you would expect that after the += operator is used, the value of checking has increased by 50. Using the implementation in Example 8-15, this is exactly what happens. Look at the function definition for the += operator:

Balance& operator+=(double other) {
 val_ += other;
 return(*this);
}

For an assignment operator, the parameter list is what will be supplied to the operator as its righthand side; in this case, an integer. The body of the function is trivial: all we are doing here is adding the argument to the private member variable. When all the work is done, return *this. You should return *this from assignment and arithmetic member operators so they can be used as expressions whose results can be the input to something else. For example, imagine if I had declared operator+= this way:

void operator+=(double other) { // Don't do this
 val_ += other;
}

Then someone wants to use this operator on an instance of my class somewhere and pass the results to other function:

Balance moneyMarket(1000.00);
// ...
updateGeneralLedger(moneyMarket += deposit); // Won't compile

This creates a problem because Balance::operator+= returns void, and a function like updateGeneralLedger expects to get a Balance object. If you return the current object from arithmetic and assignment member operators, then you won't have this problem. This doesn't apply to all operators though. Other operators like the array subscript operator [] or the relational operator &&, return an object other than *this, so this guideline holds for just arithmetic and assignment member operators.

That takes care of assignment operators that also do some arithmetic, but what about arithmetic that doesn't do assignment? The other way to use an arithmetic operator is like this:

int i = 0, j = 2;
i = j + 5;

In this case, j is added to 5 and the result is assigned to i (which, if i were an object and not a native type, would use i's class's assignment operator), but j is unchanged. If you want the same behavior from your class, you can overload the addition operator as a standalone function. For example, you might want statements like this to make sense:

Balance checking(500.00), savings(100.00), total(0);
total = checking + savings;

You can do this in two steps. The first step is to create the function that overloads the + operator:

Balance operator+(const Balance& lhs, const Balance& rhs) {
 Balance tmp(lhs.val_ + rhs.val_);
 return(tmp);
}

This takes two const Balance objects, adds their private members, creates a temporary object, and returns it. Notice that, unlike the assignment operators, this returns an object, not an object reference. This is because the object returned is a temporary, and returning a reference would mean that the caller has a reference to a variable that is no longer there. This won't work by itself though, because it needs access to the private members of its arguments (assuming you've made the data members nonpublic). To allow this, the Balance class has to declare this function as a friend:

class Balance {
 // These have to see private data
 friend Balance operator+(const Balance& lhs, const Balance& rhs);
 // ...

Anything declared as a friend has access to all members of a class, so this does the trick. Just remember to declare the parameters const, since you probably don't want the objects modified.

This gets you most of the way, but you're not quite all the way there yet. Users of your class may put together expressions like this:

total = savings + 500.00;

This will work with the code in Example 8-15 because the compiler can see that the Balance class has a constructor that takes a float, so it creates a temporary Balance object out of 500.00 using that constructor. There are two problems with this though: the overhead with creating temporary objects and Balance doesn't have a constructor for each possible argument that can be used with the addition operator. Let's say you have a class named transaction that represents a credit or debit amount. A user of Balance may do something like this:

Transaction tx(-20.00);
total = savings + tx;

This won't compile because there is no operator that adds a Balance object and a transaction object. So create one:

Balance operator+(const Balance& lhs, const Transaction& rhs) {
 Balance tmp(lhs.val_ + Transaction.amount_);
 return(tmp);
}

There is some extra legwork though. You have to declare this operator a friend of the transaction class, too, and you have to create an identical version of this that takes the arguments in the opposite order if you want to be able to use the arguments to + in any order, or if you want the operation to be commutative, i.e., x + y = y + x:

Balance operator+(const Transaction& lhs, const Balance& rhs) {
 Balance tmp(lhs.amount_ + rhs.val_);
 return(tmp);
}

By the same token, if you want to avoid the extra temporary object that is created when a constructor is invoked automatically, you can create your own operators to deal with any other kind of variable:

Balance operator+(double lhs, const Balance& rhs) {
 Balance tmp(lhs + rhs.val_);
 return(tmp);
}
Balance operator+(const Balance& lhs, double rhs) {
 Balance tmp(lhs.val_ + rhs);
 return(tmp);
}

Again, you need to create two of them to allow expressions like this to work:

total = 500.00 + checking;

In this case, the construction of a temporary object is small and, relatively speaking, inexpensive. But a temporary object is still a temporary object, and in simple, single statements, it won't create noticeable overhead, but you should always consider such minor optimizations in a broader contextwhat if a million of these temporary objects are created because a user wants to increment every element in a vector? Your best bet is to know how the class will generally be used and measure the performance overhead if you aren't sure.

It is reasonable to ask, at this point, why we need to create standalone functions for these nonassignment arithmetic operators, and not just use member functions as we did with assignment. In fact, you can declare these kinds of operators as member functions on the class you are interested in, but it doesn't make for commutative operators. To make an operator commutative on a user-defined type, you would have to declare it as a member function on both classes that could be involved in the operation, and that will work (albeit with each of the classes knowing about each other classes internal members), but it won't work for operators that you want to use with native types unless there are constructors that can be used, and even in that case, you have to pay for the temporary objects.

Operator overloading is a powerful feature of C++, and like multiple inheritance, it has proponents and critics. In fact, most other popular languages don't support it at all. If you use it with care, however, it can make for powerful, concise code that uses your class.

Most of the standard operators have some conventional meaning, and in general, you should follow the conventional meanings. For example, the << operator means left-bit shift, or it means "put to" if you are dealing with streams, as in:

cout << "This is written to the standard output stream.
";

If you decide to override << for one or more of your classes, you should make it do one of these two things, or at least something that is analogous to them. Overloading an operator is one thing, but giving an operator an entirely new semantic meaning is another. Unless you are introducing a new convention that is ubiquitous throughout your application or library (which still doesn't mean it's a good idea), and it makes good intuitive sense to someone other than you, you should stick with the standard meanings.

To overload operators effectively, there is a lot of legwork. But you only have to do it once, and it pays off every time you use your class in a simple expression. If you use operator overloading conservatively and judiciously, it can make code easy to write and read.

See Also

Recipe 8.13

Building C++ Applications

Code Organization

Numbers

Strings and Text

Dates and Times

Managing Data with Containers

Algorithms

Classes

Exceptions and Safety

Streams and Files

Science and Mathematics

Multithreading

Internationalization

XML

Miscellaneous

Index



C++ Cookbook
Secure Programming Cookbook for C and C++: Recipes for Cryptography, Authentication, Input Validation & More
ISBN: 0596003943
EAN: 2147483647
Year: 2006
Pages: 241

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net