Andrew Jenner's WebLog

Modern programming techniques

Operator overloading

Continuing along the lines of the “say what you mean“ style of programming, let's look at a naming convention for certain types of functions which more accurately reflects what they mean. Last time, we saw a function for adding two 128 bit numbers:

int128 sum_of_128bit_numbers(int128 x,int128 y)
{
    int128 z;
    z.low = x.low + y.low;
    z.high = x.high + y.high;
    if (z.low < x.low)
        ++z.high;
    return z;
}

and some code which calls it:

    z = sum_of_128bit_numbers(x,y);

One way we can improve this is to write the function as an overloaded operator:

int128 operator+(int128 x,int128 y)
{
    int128 z;
    z.low = x.low + y.low;
    z.high = x.high + y.high;
    if (z.low < x.low)
        ++z.high;
    return z;
}

Then the syntax to call it is just:

    z = x + y;

which I'm sure you'll agree is much simpler, especially when you have lots of overloaded operators and a complex expression. Which would you rather read/write/review/maintain, this:

    z = x*x + y*y;

or this:

    z = sum_of_two_128_bit_numbers(product_of_two_128_bit_numbers(x,x),product_of_two_128_bit_numbers(y,y));

?

Programmers who are used to a programming language which doesn't offer operator overloads tend to be rather horrified at this concept the first time they see it (I know I was). They say things like “but this means I can't tell what even the simplest pieces of code really do just by looking at them”. I'd argue that this ability is overrated. Modern programming is all about abstraction. We don't need to know every single detail about what's going on in a particular line of code. We don't need to know the mechanics of how addition works to understand a line of code like:

    z = x + y;

We also don't need to know which registers the compiler decides to assign to which variables (if we did we'd be writing in assembly language, not C++). Nor do we need to know how numbers are represented in binary by the computer's hardware, nor the details of the logic gates and transistors used in the CPU's addition circuitry, nor the voltage levels in the processor nor any one of a million little details which all have to work out in order that we can add two numbers. All we need to know is the highest level concept - we're adding these two numbers. And that fact is best expressed by the statement:

    z = x + y;

Now, I do agree that it is possible to abuse that power and write code which is almost impossible to follow, for example by writing an “operator+” function that does something entirely different from addition. But you can write bad code in any language. You can do similarly evil things even in C:

int128 sum_of_128bit_numbers(int128 x,int128 y)
{
    int128 z;
// Haha! We lied and will actually XOR the two numbers! z.low = x.low ^ y.low; z.high = x.high ^ y.high; return z; }

The problem here is that the function doesn't do what it says, not that operator overloading is intrinsically evil. The name “operator+“ is just that - a name. It's a name which implies certain things about the semantics of the function in question, just like any good name should. It also allows callers of this code to employ certain syntactic sugar - they can write:

    z = x + y;

instead of:

    z = operator+(x,y);

(The latter is completely valid C++ code, by the way, though rarely more appropriate than the former).

There is one other argument against operator overloading - that is that it makes it more difficult to see the performance bottlenecks in a program. Maybe you have an “operator+” which adds large matrices together, and could potentially be quite slow if the matrices are large. You wouldn't necessarily expect a simple “x+y” expression to be slow, if you're used to programming in C where such expressions typically cause the compiler to emit only one or two machine language instructions.

The counter-argument is that we should not expect the names of functions to reflect how long they take to run (otherwise we'd be writing functions with names like “add_two_128bit_numbers_takes_about_20_cycles_to_run”) which would be a maintainance nightmare (imagine if every time you changed the implementation of a function you had to change the name of that function and every function that called it!). No, use the right tool for the job - visually inspecting a program isn't the right way to figure out where the performance bottlenecks are. Use a profiler instead.

Published Thursday, August 05, 2004 3:05 PM by ajenner

Comments

 

Rick Schaut said:

Well, there is the potential problem of implicit type conversions causing unexpected results, but that's what the "explicit" keyword is for.
August 5, 2004 4:11 PM
 

Larry Osterman said:

Andrew, Rick's right. Also, what happens when one of your overloaded operators throws?

How many objects are created when you do:

string a;
string b;
string c;
string d;

c = a + b + "test" + d;

?

How many heap allocation/frees? How meny memory copies? What happens if any of those assignments fails? What are the thread safety implications?

Operator overloading is NOT something that you should implement in production code unless you REALLY know what you're doing.
August 5, 2004 4:16 PM
 

Andrew Jenner said:

True, one must take care when defining single-argument constructors (this is where "explicit" is often needed).
August 5, 2004 5:25 PM
 

Andrew Jenner said:

Well, one should never change anything in production code unless one REALLY knows what one's doing! Operator overloading is no exception, but it as C++ features go it isn't that difficult to use in a safe manner, nor is it intrinsically slow.

> what happens when one of your overloaded operators throws?

> What happens if any of those assignments fails?

Writing exception safe code is a big topic, and something I'm going to be covering in great detail in later columns.

> How many heap allocation/frees? How meny memory copies?

Irrelevant. Don't even bother to count such things, as it's almost certainly a premature optimization. Instead, write code that follows the "say what you mean" axiom, and if it turns out to later to be too slow, profile it and concentrate your optimization efforts on the bottlenecks.

However, rewriting this as:

c = concatenate(a,concatenate(b,concatenate(string_from_cstr("test"),d)));

isn't going to help performance one iota - the only thing it will do is hurt the readability of your code.

> What are the thread safety implications?

If you're using any objects on multiple threads without proper locking semantics you're going to be in trouble, operator overloading or no operator overloading.
August 5, 2004 5:26 PM
 

AndrewSeven said:

RE :"They say things like 'but this means I can't tell what even the simplest pieces of code really do just by looking at them.' I'd argue that this ability is overrated"

I'm a bit of fanatic about clear code, I don't think the ability is overrated, but I don't think the argument aplies

When overloading operators, it is important to respect the meaning + is addition - is subtraction and so on; just about everyone can agree on the semantics.

As long as you respect the semantics, z = x + y where x y znd z are int128 then it is just as clear as z = sum_of_128bit_numbers(x,y), maybe even clearer.

But if you want to implement:
widget = grapple + grommet;
and they are two or three different classes, I'll have objections.
August 5, 2004 6:06 PM
 

Andrew Jenner said:

I absolutely agree - sorry I didn't make this clearer in my post. The semantics of "operator+" should always be "addition" (or at a stretch "string concatenation" since that's somewhat of a standard meaning).

The function "operator+" should not be implemented for objects which it doesn't make sense to add together - doing so would be just as evil as the "add which actually does an XOR" function above.

This is a rule which is actually broken by the C++ standard library itself - the bit-shift operators ("<<" and ">>") were redefined to inject and extract objects from streams respectively. I think this is a case of "do as we say, not as we do" on their part.

As the new meaning is now pretty familiar to anyone with even a cursory knowledge of C++, it isn't really a problem. But it does kind of set a bad example to other authors of C++ class libraries, whose libraries will almost certainly not be as popular as the C++ standard library. If such authors similarly make up new meanings for operators, code that uses these overloads could be quite difficult to understand.
August 5, 2004 6:22 PM
 

Vatsan Madhavan said:

>How many heap allocation/frees? How meny
>memory copies?

I like this prototype much better:

int128 operator+(const int128& x, const int128& y);

Fewer object copies
Less overhead during function call
Is more accurate in describing the intention of code - i.e. operator+ doesn't just add two entities, it also guarantees that the arguments will stay intact (which *is* usually the case in most overloads of +)

August 5, 2004 8:19 PM
 

Andrew Jenner said:

Good point - thanks! (Though passing by value instead of reference also guarantees that the arguments will stay intact - but I know what you meant.)
August 5, 2004 8:22 PM
 

Vatsan Madhavan said:

>>c = a + b + "test" + d;
...
>>How many heap allocation/frees? How meny
>>memory copies? What happens if any of those
>>assignments fails? What are the thread
>>safety implications?

I don't think these questions are exclusively relevant to operator overloading itself. So the example (of string concatenation), IMO, is a red-herring.

The concatenation simply gets transformed to:

operator+(operator+(operator+(a,b),"test"),d)

which is no better than having named the function concatenate_two_strings(), and we would have had:
[notepad, paste, s/operator+/concatenate_two_strings]

concatenate_two_strings(concatenate_two_strings(concatenate_two_strings(a,b),"test"),d)

and the questions you asked continue to remain unanswered..


August 5, 2004 8:32 PM
 

Larry Osterman said:

Just getting back to this thread.

Here's the problem with heap allocations. By default, the NT heap has a single lock (in its implementation on XP - on W2K3 and on there are lock-free options that dramatically reduce this issue).

If you're running on a single threaded app, it doesn't matter. If, however you're on a multi-threaded app, this heap critical section can (and often does) quickly become the scalability bottleneck in your application.

You say that I shouldn't care about such details. But it's my JOB to care about such details. Why? Because the code I write goes into Windows. And it'll be executed by hundreds of millions of people. And if I've got a scalability problem designed into my application, then it's THEIR problem, not mine.

If I design my classes with operator overloads that hide trips through the heap, then I've introduced a scalability bottleneck into my application. And it's one that's darned hard to find (you have to go through all the code seeing if it allocates memory). And will be even harder to remove.

Ask someone who's been on the NT for a while (more than 12 years) about GDI in NT 3.1. It was written in C++, with operator overloads by people who decided that the number of copies introduced in code was irrelevant because they wanted to "say what they meant".

And all that code had to be rewritten because it was too slow. Now C++ compilers are a lot better today than they were 15 years ago (the GDI guys weren't even writing to a native C++ compiler, they were using cfront to convert C++ to C). But trusting your compiler to save you is no excuse for bad coding.

As far as I'm concerned, here's the bottom line: You MUST know what assembly language code is being generated by your constructs. If you haven't stepped through the code in the debugger IN ASSEMBLY MODE then you don't know what stuff's hidden where you don't know about it. And it's WAY too easy to mess things up.
August 6, 2004 12:05 AM
 

Mark Mullin said:

One observation about function naming and speed, since you're using math examples - I've developed the tradition to use _exact and _approx as trailing modifiers for implementing functions to indicate whether or not the function is oriented towards speed or accuracy - I agree wholeheartedly with the comments you make, just wanted to observe that, especially with respect to math routines, function names can often be used to disambiguate performance intent, especially when both variants are needed

re Larry's comments

The argument applies from a sysprog level, but for many appprogs, the associated engineering expense just ain't worth it. Nevertheless, as far as sysprog contexts, you can factor the usage construct, say an inline overloaded operator call, with the implementation issues, say embedded asm used to represent the body of the function - then you get the best of both worlds, the clarity and convienience of the C++ operator overload and the raw performance of your hand tuned assembly - I've done this for implementing a high performance vectorizing library for supporting 3D visualization stuff - and if you want to be --really disgusting--, iff your code is coming off a server using fusion or one-click, you can even consider the dynamic assembly of logic geared to inspection of the target machine - i.e. your loader checks cpu type and lets the code server know, which then swaps in and out routines based on optimal performance for that cpu - I admit that while I've gotten the trick to work, I've never actually dared use it in a general release :-(


August 6, 2004 5:44 AM
 

Eusebio Rufian-Zilbermann said:

I was used to "say what you mean" but then I came across C#

Try this code:-

short s1=1;
short s2=2;
short s3 = s1+s2;

The compiler will give you an error if you write that in one of your functions. Is it "a bug or a feature"?
August 6, 2004 8:47 AM
 

Andrew Jenner said:

Larry, I guess I should have made it clearer from the start that this blog is mostly about high-level programming (i.e. applications programming) where making the code easily readable and maintainable is more important than squeezing out every last cycle.

I quite understand that there are some pieces of code (particularly in core OS systems and low-level run time libraries, or in older code from when C++ compilers weren't so good at optimizing) where the other trade off is the right thing to do, but for almost all new code this is not the case.

I disagree that every programmer should step through every opcode the compiler generates for all the code they write. While it is often useful (especially in situations where performance is important) to be able to predict the sort of code the compiler is likely to generate, I can't think of a single bug in any of my code that would have been found this way.

Generally, programmers writing high level code just don't need to know what the generated assembly is, as long as it does the right thing and isn't too slow. We have compilers in order that such things can be abstracted away and to prevent us from having to worry about them.

Also, if your compiler is "hiding" unexpected stuff in the binaries that you can only find by examining the generated code, it seems to me that's a rather serious problem in the compiler you're using!

As for heap lock contention in multi-threaded *apps*, there are many options which allow the dirty work to be abstracted away without having to sacrifice "say what you mean". For example, using per-thread memory arenas for the classes where this is an issue. Or reference-counted, copy-on write objects. Or objects which double the amount of memory they allocate every time they run out, reducing the number of allocations needed to log(n). Abandoning high-level programming techniques altogether and writing everything "close to the metal" should be a last resort.


Mark, the "_exact"/"_approx" modifiers are an interesting technique I haven't seen before - thanks!


Eusebio, I would call that a bug (actually more of a design flaw) in C# - yet another reason to prefer C++. I didn't believe you, so I had to check for myself! There a number of problems here which conspire to make this confusing:
1) The .NET IL doesn't support arithmetic operations on types smaller than 4 bytes.
2) This implementation detail is exposed as an omission in the overloads for the "+" operator, breaking the abstraction of the "short" type.
3) C# helpfully converts both operands to "int"s without telling you that it's doing so, or why.

I understand why the designers of the language did this (implementing a full set of functionality for "short" was probably put aside in favor of implementing functionality that would be useful to more people) but I agree that it's ugly. I've filed a bug against the designers of C# on your behalf.
August 6, 2004 9:46 AM
 

Atilla Ozgur said:

Say what you mean is better programming practice from view of application programming. Although I know assembler and looked at the ILAsm. When you are writing business application you have deadlines that is way I am writing code in saturday to keep my promise. A code which is easier to read and works better than the code which is faster and more correct when you are coding Bus App.

Operator overloading is one of the good features of C++/C#. In whidbey VB.NET will have it too. I did not understand your usage of "_exact"/"_approx" in Math application. Could you explain it better.
August 7, 2004 4:38 AM
 

Andrew Jenner said:

I often find myself having to write code on Saturdays to keep my deadlines as well - it seems to be standard practice in the industry.

However, I find that writing code that is easier to read actually saves time (since it tends to require less debugging) and tends to be more correct (since it's easier to tell when it's wrong just by looking at it). It shouldn't have to be a trade off.

What Mark was talking about with the "_exact"/"_approx" thing is this: suppose you're writing a math library which is likely to be used both in games (which require speed but where accuracy is less important) and also scientific simulations (where accuracy is crucial but speed less so). You might have two versions of many of your functions for the two purposes:
sqrt_approx()
for the games and
sqrt_exact()
for the science.
August 7, 2004 10:02 AM
 

Mark Mullin said:

High marks for Andrew - the most extreme example of this comes from Jim Blinn (obligatory genuflection) who is now at msft research - he pointed out that in many 3D calcs calling for a square root, dividing by two would suffice when dealing with relatively small quantities -
August 9, 2004 6:10 AM
 

Andrew Jenner s WebLog Operator overloading | home lighting said:

June 13, 2009 9:53 PM
Anonymous comments are disabled

This Blog

Syndication

Tags

No tags have been created or used yet.

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker