Overloading and Overloading

Operator overloading may be syntactic sugar, but there are a lot of things that don't taste very good without sugar.


April 01, 2006
URL:http://www.drdobbs.com/overloading-and-overloading/184406484

Pete is a consultant specializing in library design and implementation. He has been a member of the C++ Standards Committee since its inception, and is Project Editor for the C++ Standard. He is writing a book about the newly approved Technical Report on C++ Library Extensions; the book will be published this summer by Addison-Wesley. Pete can be contacted at [email protected].


Last December, I was in the Ground Transportation Center at the Indianapolis Airport waiting for the limousine that would take me on the last leg of my move to Bloomington, Indiana, when I got a call on my cell phone from Jon Erickson. He told me that the C/C++ Users Journal had ceased publication and that my next column, due in a couple of days, would not be needed.

I'd just spent two days with movers packing up everything from my house in Arlington, Massachusetts, and loading it onto a truck. On top of that, my fiancée and I had just bought a house; I was selling mine; I had just sent a preliminary draft of my book out for technical reviews; and I was in the middle of rearranging my employment relationship. So I was rather overloaded, and relieved to not have that next installment lurking on my to-do list.

Things have settled down now, and most of the stress of moving has gone away. Jon and I have been talking during the past month about this column, and I figure it's a great way to build that overload back to a peak. I've been reading Dr. Dobb's Journal for many years, dating back to the days when it was "Dr. Dobb's Journal of Computer Calisthenics and Orthodontia: Running Light without Overbyte." I'm pleased to be a part of it now.

This column is about solving C and C++ problems. Of course, that often means showing various coding tricks and programming techniques. But there are times when we just can't code our way out of a problem—changing the code around just creates a new set of problems, and we end up playing Whack-a-Mole instead of making progress. When we've painted ourselves into corners like this, the thing to remember is that there really isn't any wet paint. We can just leave. Throw the whole mess out, write off the time spent as an educational expense, and start over. I can usually get things right about the third time through.

This particular column is about overloading: both in the technical sense of writing multiple functions with the same name and leaving it to the compiler to figure out which one to call, and in the non-technical sense of giving the compiler so much to do that it gets overwhelmed. Combining templates with overloaded functions can cause breakdowns. Sometimes, the way to avoid these breakdowns is to get rid of the overloads.

Operator Overloading

Back in the late '80s, I was a C addict working at Borland International on its C compiler. There was a rumor that we might be moving to C++, so I decided to learn a little about it. I started reading the first edition of Bjarne Stroustrup's well-known book, The C++ Programming Language. I don't have a copy handy, but I have a vivid memory of seeing, around the second page, code something like this:

#include <iostream.h>
int main()
{
cout << "Hello, world\n";
return 0;
}

That use of the left-shift operator looks pretty pedestrian today, but 20 years ago, it was radical. I put the book aside and went back to my real work.

A couple months later, the rumor became fact and I picked up the book again. I still wasn't comfortable with that left-shift, but over time, I've gotten used to it, and now it looks even more natural than in its normal C usage.

Java zealots dismiss operator overloading as "syntactic sugar," but that's because they don't have it. The alternative is the named function, so Java code for arithmetic types ends up looking something like this:

BigInteger first = new BigInteger(1);
BigInteger second = new BigInteger(1);
BigInteger sum = first.add(second);

Compare that with the analogous C++ code, using an overloaded operator+:

BigInteger first = 1;
BigInteger second = 1;
BigInteger sum = first + second;

While it's certainly possible to learn to read the Java version, the C++ version looks much more like the natural formulation of the original problem. It also looks much more like the code for the same computation with built-in types that, in both languages, can be written like this:

int first = 1;
int second = 1;
int sum = first + second;

If you still think that syntactic sugar doesn't matter, imagine lemonade without sugar.

Function Overloading

Operators are just functions with funny names [1]. Once you get used to those funny names, operator overloading is just a part of function overloading. Function overloading occurs when you write two or more functions with the same name. For the compiler to tell them apart, they have to have different argument lists. For example, the C++ Standard Library provides three overloaded versions of the sin function, one for each of the three built-in floating-point types. So you can write code like this:

sin(1.0F); // calls float sin(float)
sin(1.0); // calls double sin(double)
sin(1.0L); // calls long double sin(long double)

I don't find this example particularly compelling. In every case I've run into, the C technique of using functions with different names works just fine:

sinf(1.0F); // calls float sinf(float)
sin(1.0); // calls double sin(double)
sinl(1.0L); // calls long double sinl(long double)

Furthermore, if you want to call sinl with a value of type double in C, you just do it:

double x = 1.0;
sinl(x); // calls long double sinl(long double),
// promotes 1.0 to type long double

To call the long double version of sin in C++, you must provide an argument of type long double:

double x = 1.0;
sin((long double)x); // calls long double
// sin(long double)

If you're compulsive about new-style casts, this becomes even more long winded:

double x = 1.0;
sin(static_cast<long double>(x));

While new-style casts may well provide useful benefits in general coding, in complex mathematical computations, they introduce unnecessary clutter. Fortunately, C++ retains the C versions of sin, so you can call the long double version, sinl, directly, just as in C.

On the other hand, with trig functions, we're dealing with a small set of argument types, so remembering and using the three C names isn't hard. But when you have a larger set of types, function overloading does simplify things. To continue with examples from mathematics, in TR1 we have the following versions of pow:

double pow(double, double);
float pow(float, float);
long double pow(long double, long double);
double pow(double, int);
float pow(float, int);
long double pow(long double, int);

TR1 also provides the C99 versions of pow, named powf and powl, which correspond to the second and third versions in this list. It's certainly possible to come up with reasonable naming conventions that provide distinct names for all six of these functions, but keeping track of their names would contribute to the mental overloading that makes programming harder.

Templates and Function Overloading

While overloading mathematical functions is convenient, overloading functions for use in templates is essential. Consider a rather pointless function that exchanges the contents of its second and third arguments if its first argument has the value true:

template <class Ty>
void exchg(bool do_it, Ty& arg1, Ty& arg2)
{
if (do_it)
{
Ty temp = arg1;
arg1 = arg2;
arg2 = temp;
}
}

This function works when called with two objects of any type Ty that can be copy constructed and assigned. But there are types that can be exchanged more efficiently by a function that knows how they are implemented. For example, the Standard Library template vector holds a pointer to an array of objects. Exchanging two vector objects by copy constructing and assigning means copying their arrays three times. Exchanging two vector objects by swapping their pointers around doesn't require any copying. So the swap operation is encapsulated in the Standard Library's template function swap, which can be implemented like this:

template <class Ty>
void swap(Ty& arg1, Ty& arg2)
{
Ty temp = arg1;
arg1 = arg2;
arg2 = temp;
}

With this version of swap, we can write our exchange function like this:

template <class Ty>
void exchg(bool do_it, Ty& arg1, Ty& arg2)
{
if (do_it)
swap(arg1, arg2);
}

Now, that doesn't look like much of an improvement. In fact, it looks worse because we have two functions instead of one simple one. The benefit comes when there is a specialization of swap, for the type Ty, that is more efficient than the generic one. For example, the Standard Library has a partial specialization of swap that takes arguments of type vector<Ty>:

template <class Ty>
void swap(vector<Ty>& arg1, vector<Ty>& arg2)
{
arg1.swap(arg2); // magic...
}

The magic member function vector::swap is part of the implementation of vector. It can be written with knowledge of how vector is implemented, so it can swap pointers instead of copying arrays.

When we call exchg with two arguments of type vector<T>, the compiler generates code that calls this partial specialization of swap instead of the generic version. When we call exchg with two arguments of type int, the compiler generates code that calls the generic version because there is no specialization of swap that takes two arguments of type int.

Granted, partial template specialization isn't function overloading in its technical sense. It's a convenient example, though; and when you use a type that isn't a template, you can provide an overloaded version of swap that will be used instead of the generic version wherever swap is called for your type.

Problems with Overloading

Say you're writing a function that displays an integer value and a complex value to the console. Something like this (assuming we have all the necessary #include directives):

int i = 3;
complex<double> c(1.0, 0.0);
cout << i << '\n';
cout << c << '\n';

Now, you make a seemingly innocuous change to write the same data to a temporary file, opened on the fly:

int i = 3;
complex<double> c(1.0, 0.0);
ofstream("data.log") << i << '\n';
ofstream("data.log") << c << '\n';

Perhaps surprisingly, this code shouldn't compile. The last line is illegal. The reason lies in the declarations of the overloaded shift-left operators.

The class ofstream is derived from basic_ofstream<char>, and basic_ofstream defines a member function to do the insertion:

template <class Elem, class Tr =
char_traits<Elem> >
class basic_ostream :
virtual public basic_ios<Elem, Tr>
{
// ...
basic_ostream& operator<<(int val);
// ...
};

The shift-left operator for complex types is not a member function. It looks something like this:

template <class Ty, class Elem, class Tr>
basic_ostream<Elem, Tr>& operator<<(
basic_ostream<Elem, Tr>&, const complex<Ty>&);

When you call either of these overloaded operators with cout as the output stream, everything is fine. When you create a temporary object such as ofstream("data.log"), the two functions act differently. That's because the temporary object is not an lvalue, and in general, to pass an argument by nonconst reference, you must have an lvalue. The reason for that rule is mostly caution: If you accidentally create a temporary object, you probably don't want to pass it to a function that's going to modify it because the temporary object will be destroyed when the function returns. There's an exception to this rule, though, for member functions: You can use a nonlvalue as the object for any member function, even if that member function modifies the object. The operator that inserts an int is a member function, so calling it with a temporary is okay. The operator that inserts a complex value is not a member function, so calling it with a temporary is illegal. So be careful how you define overloaded operators and how you use them.

Another problem arises from an interaction between templates and overloaded functions. Suppose you're writing a template function that takes two arguments. The first argument is a pointer to a function that, in turn, takes an argument of some type and returns a value of that type. The second argument is a value, not necessarily the same type as the argument type for the function pointer. The job of your template function is to call the function pointer, passing the value and returning the result. Like this:

template <class Ty0, class Ty1>
Ty0 apply(Ty0(*fp)(Ty0), Ty1 val)
{
return fp(val);
}

Simple enough [2]. And being an unrepentant C programmer, you try it out like this:

apply(sinf, 1.0);
apply(sinl, 1.0);

No problem. Then you try this:

apply(sin, 1.0);

Now there's a problem. The compiler doesn't know which of the three versions of sin you want. The call is ambiguous. You've been bitten by overloading. The solution, such as it is, is to tell the compiler what to do. You do that with a cast:

apply((float(*)(float))sin, 1.0);

Or, if you're going to be doing this more than about once, with a typedef and a cast:

typedef float(*fp_func)(float);
apply((fp_func)sin, 1.0);

Or, perhaps:

typedef float (*fp_func)(float);
fp_func ptr = sin;
apply(ptr, 1.0);

All that, just to undo the overloading.

Overloading is not an unmixed blessing. It can make code harder to read, and it can require more verbose coding. It can even bog down the compiler. When you're about to write an overloaded function, ask yourself if it's really needed. If the answer is no, you might save yourself from being overloaded, too.

Notes

[1] Yes, and funny syntax, too. But when we write overloaded operators, we write functions with funny names. It's only when we call them that we use notation that's different from ordinary functions. And even that isn't always true. You can call an overloaded operator by name, but it's almost always more appropriate to use the operator itself.

[2] But important. This general scheme is the core of call wrapper types that encapsulate and hide the differences between various callable objects. The C++ Standard Library, for example, provides call wrappers such as bind1st and bind2nd.

DDJ

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.