Operator overloading

Part of C++ FQA Lite. To see the original answers, follow the FAQ links.

This section is about operator overloading - a way to make the code "readable" as long as the reader doesn't care what the code actually does.

[13.1] What's the deal with operator overloading?

FAQ: Overloaded operators provide an "intuitive interface" for the users of your class. They also allow templates to work "equally well" with classes and built-in types.

The idea is to call functions using the syntax of C++ operators. Such functions can be defined to accept parameters of user-defined types, giving the operators user-defined meaning. For example:

Matrix add(const Matrix& x, const Matrix& y);
Matrix operator+(const Matrix& x, const Matrix& y);
Matrix use_add(const Matrix& a, const Matrix& b, const Matrix& c)
  return add(a,add(b,c));
Matrix use_plus(const Matrix& a, const Matrix& b, const Matrix& c)
  return a + b + c;

FQA: Operator overloading provides strong source code encryption (the time needed to figure out what a+b actually means is an exponential function of the number of types, implicit conversions, template specializations and overloaded operator versions involved).

It is also one way to have templates malfunction equally miserably with user-defined and built-in types - if BraindeadPseudoTypeSafeIterator<T> supports the syntax *p and ++p, it can be used just like the built-in T* in a template "algorithm" like std::for_each. However, it could also work without operator overloading - for example, you can have a global increment function for all "iterator" types, and implement it for all pointer types using a global template wrapper calling ++p. Which means that operator overloading is pure syntactic sugar even if you don't consider templates a pile of toxic waste you'd rather live without.

This syntactic sugar can only be added to a language like C++ together with more than a grain of salt. For example, what if a+b fails? There's no good way to return an error status, because the language is "strongly typed", so a+b always returns objects of the same type, for example, a Matrix. There's no natural "bad matrix value", and anyway the whole point of a+b is that you can also write a+b+c. If you need to test each addition with an if statement, a+b is not much better than add(a,b). What about throwing an exception upon error? Not a bad idea, in a language supporting exceptions, as opposed to merely providing non-local goto using throw/try/catch syntax and having you care about "exception safety" in each and every bit of code.

What about the allocation of our result - where does that Matrix live? If it's allocated dynamically with new and returned by reference, who is supposed to clean it up, in particular, who keeps the list of all temporaries created by a+b+c...? But if it's returned by value, then the copy constructor is going to copy lots of matrices. Bad if you care about performance. And if you don't, you surely wouldn't mind the smaller run time overhead of garbage collection (not to mention run time boundary checking and other kinds of safety belt), so you'd probably choose a different language in the first place.

Operator overloading is not necessarily a bad idea - if you can actually keep the promise about "readability". To do that, you need at least three things: comprehensible overload resolution rules, easy-to-use exceptions and easy-to-use automatic memory management. C++ offers none of those.

[13.2] What are the benefits of operator overloading?

FAQ: You can "exploit the intuition" of the users of your classes. Their code will speak in the language of the problem instead of the language of the machine. They'll learn faster and make less errors.

FQA: The keyword in the FAQ's answer is "exploit". Your intuition tells you that a+b just works - and in the "problem domain", you are right. But in C++, it doesn't take the machine too long to raise its ugly iron head and start talking to you in its barbaric language, and it has all the means to make you listen.

If you want to program "in the problem domain", use a language designed for that problem domain. Prototype numerical algorithms in Matlab or the like, not in C++ with some matrix library. Design hardware in Verilog or the like, not SystemC (which is C++ with some hardware description primitives library).

The next best alternative is to use a general-purpose language where operator overloading and other metaprogramming features actually work. It is typically worse, but may be cheaper - special-purpose tools are used by less people than general-purpose ones, so the vendor must charge each user a higher price. And it will make you happy if you're one of those people who think that there's nothing a special-purpose language can do that can't be done equally well or better using the wonderful metaprogramming facilities of the awesome language X (typically wrong, but strong programmers can be very productive operating under this assumption - unless X=C++).

Operator overloading and other features sure make C++ equally adaptable to any problem domain. This is achieved by making it the wrong tool for every job.

[13.3] What are some examples of operator overloading?

FAQ: Here are a few (some real, some hypothetical), there are many more.

FQA: C++ operator overloading is a bad way to implement all of these things. It's an equally bad way to implement almost everything that comes to mind.

Basically most operator overloading use cases fall into one or more of the following categories:

However, it doesn't mean that operator overloading is never useful - that is, in the languages that can actually support it.

[13.4] But operator overloading makes my class look ugly; isn't it supposed to make my code clearer?

FAQ: No - it's supposed to make the code using your class clearer! For example, you may claim that T& operator[](int i) is less readable than T& getAt(int i), but surely arr[i] is more readable than arr.getAt(i)!

You should realize that "in a reuse-oriented world", usually (yes, the FAQ says "usually") many people will use your class, but only one writes its code (that one person is you). Think about the good of the majority - your users!

FQA: Those guys living in the "reuse-oriented" worlds are very noble characters. Too bad we're stuck on planet Earth, which didn't seem to have any particular "orientation" last time I checked. On this planet, most classes you write are used exclusively by yourself, the majority of the rest is used by two or three people, and once in a while you get to write a class to be used by N people for N>3 - typically it's not an array class, but something with a little bit less trivial functionality. Here on Earth, this is considered good: the more interaction between people and components, the more errors there are likely to be (insert the FAQ's favorite pseudo-business-oriented statements about "defect rate" here).

Of course this doesn't mean that the other claims make any sense. For example, users have to figure out what your class does before they use it, so its interface must be defined in a readable way, not just look readable when used. And your users will probably need to understand your implementation, too, because even when your code no longer has bugs (this might take a while), bugs in other pieces of code may end up corrupting your data (this is C++), so people will have to inspect what's left of that data in order to find the original error. So an unreadable and/or complicated implementation is not a very good idea even in a "reuse-oriented" world.

Last but not least, arr.getAt(i) is not that bad.

[13.5] What operators can/cannot be overloaded?

FAQ: Most can be overloaded. You can't overload the C operators . and ?: (and sizeof). And you can't overload the C++ operators :: and .*. You can overload everything else.

FQA: There are other restrictions, for example there can't be a global operator[] overload - you can only overload this operator using a class member function. Which is not necessarily bad.

[13.6] Can I overload operator== so it lets me compare two char[] using a string comparison?

FAQ: No, at least one parameter of an overloaded operator must be a user-defined type.

But even if you could do it, you shouldn't - you should use a class like std::string. Arrays are evil!

FQA: C++ doesn't let you do it for a good reason - if you could do it, how would you make sure that your program is compiled consistently in the sense that your operator is called everywhere - in all contexts calling operator== on char[] arguments? And what if two such operators are defined by two different modules?

Unfortunately, these problems are almost equally severe with user-defined types. For example, std::map doesn't have an overloaded operator<< printing it to an ostream. You can define one, but so can I, and then our libraries are linked together into a single program. The result is unpredictable, ranging from a link-time error to a program printing some maps using your code and some with mine.

As to the "arrays are evil" mantras - how about chanting them to the authors of the C++ standard library that defined an std::ifstream constructor accepting const char*, but not std::string? And they didn't define an operator const char* in std::string, either. std::ifstream in((std::string(dir) + "/" + file).c_str());. Give me a break.

And why does the FAQ say "a std::string-like class", not just "std::string"? Because if it did, the majority of the audience stuck with one (or more) of the many, many "string-like" classes developed before one was added to the C++ standard library would laugh quite bitterly.

[13.7] Can I create a operator** for "to-the-power-of" operations?

FAQ: You can't. You can only overload operators already in C++, without changing the number of arguments, the associativity or the precedence. If you think it's wrong, consider the problem of a**p, which means "a times the result of dereferencing p". If you could define operator**, you'd make such expressions ambiguous.

And you know what? Operator overloading is just syntactic sugar. You can always define a function instead. Why not overload pow?

Or you could overload operator^, which works but has the wrong precedence and associativity.

FQA: Sure, there's lots of grammar in C++, and making an unambiguous addition is almost impossible. The fact is that, informally speaking, there's a finite amount of "good" syntax you can have in your language. This is a valid argument if you advocate domain-specific languages (which can make the short, simple syntax map to stuff most useful in that domain), or if you argue that "languages should have no syntax" (Lisp, Smalltalk and Forth are examples of languages with almost no syntax). However, it's funny when this argument is used by the C++ FAQ. It is hardly compatible with the claims that C++ is applicable everywhere. And then there's the huge amount of completely pointless complications in the C++ grammar (like the constructor/function declaration puzzle).

What's that? "Operator overloading doesn't provide anything fundamental, it's just syntactic sugar"? Now you're talking. Make that "syntactic cyanide" and I'm in.

Overload pow? Can somebody tell what's the point of all this overloading? If you want obfuscation, there are tools you can use that allow you to keep the source in the unobfuscated form, which may be very handy at times. Besides, isn't it std::pow? AFAIK you are not allowed to add names to the std namespace. And if you add a global function pow, you'll lose the precious "transparency" (std::pow(x,y) will never call your version of pow, only pow(x,y) will), so what's the point?

The idea to overload "bitwise exclusive or" to mean "power" is just stupid. I wonder where they get these ideas. It's as if someone decided to overload "bitwise left shift" to mean "print to file". Wait a minute - they did that, too... Oh well.

[13.8] Okay, that tells me the operators I can override; which operators should I override?

FAQ: You want to help your users, not confuse them. Overload operators if it makes the life of your users easier.

FQA: Translation: don't overload C++ operators. While we're at it, don't overload C++ functions, either. C++ overload resolution always ends up confusing users. And you can't add error handling without using the horrible C++ exceptions, and you can't allocate objects simply and efficiently.

[13.9] What are some guidelines / "rules of thumb" for overloading operators?

FAQ: 22 (!!) "rules of thumb" are listed (actually 20, 2 just say "use common sense"). Basically, the rules are about making overloaded operators behave similarly to built-in operators - if you define +, make it associative and don't change the parameters, etc. The FAQ warns that the list is not exhaustive, nor should it be interpreted as strict rules that can't have exceptions.

FQA: Rule of thumb #23: things which have simple functionality but are very hard to get right linguistically should be in the compiler.

Use common sense. Do you have the time for all that thinking when all you get is "syntactic sugar" of questionable taste? How about implementing some user-visible functionality instead? Not only is it what people (customers, employers, colleagues, friends) ultimately care about, it's also much more fun.

[13.10] How do I create a subscript operator for a Matrix class?

FAQ: Use operator() which can get two indexes, i and j. Don't use operator[], which can only get one index. A code listing follows.

FQA: Oh, dear. You're writing a Matrix class. My condolences.

Once you define your subscript operator, be prepared to answer many more questions. How do you allocate the result of operator+? How do you map expressions like A+B*C to optimized implementations of several operations (for example, multiply_add)? Why are you writing a Matrix class with overloaded operators instead of prototyping the code in Matlab or the like and then implementing the prototype in C so that it runs fast and anybody can tell how and why from the code, instead of figuring out how A+B*C actually works?

[13.11] Why shouldn't my Matrix class's interface look like an array-of-array?

FAQ: The "array-of-array" alternative uses operator[] to return an array object, which in turn has an operator[] returning a single element.

This approach is likely to be worse because of performance issues having to do with the optimal layout of the matrix in memory, says the FAQ. Or maybe it says something else similar to it. It talks a lot about the issue.

FQA: You can't implement the array-of-array syntax with a single function call, because C++ has no operator[][]. You need to return some temporary object with an operator[].

This means extra work for you, because instead of writing a function you write a whole class and a function returning its objects. This also means extra work for the user who has to read all these interfaces. This also means extra work for the compiler. The chances of a compiler from planet Earth to optimize out the temporary objects are very close to zero. Which means it's also extra work for the machine running your code.

And what did you achieve? "Syntactic sugar". This reasoning can frequently be used to optimize the entire Matrix class with all its overloaded operators out of your schedule. And it doesn't depend on performance-related claims about memory layout and stuff.

[13.12] I still don't get it. Why shouldn't my Matrix class's interface look like an array-of-array?

FAQ: Because it makes checking the arguments or changing the data structure harder. The FAQ then explains how this can still be done, and claims that the compiler will optimize the temporaries out, but doesn't like the fact that it's a lot of work.

FQA: The FQA primarily doesn't like the fact that it's a lot of work. But it would also like to point out that: 1. The compiler will probably not optimize the temporaries out. 2. Key performance-critical data structures are extremely costly to change no matter how much "encapsulation" you use, because optimizations depend on the representation. 3. Checking the arguments is always hard with operator overloading, because what would operator() do once an error was found - return -1 (how?), set a global error flag, throw a C++ exception? All these options are not really acceptable.

Anyway, if you want to waste some time without getting useful work done, there are ways to do it that are much more fun than getting operator[][] sort of work. And this is where the FAQ and the FQA agree.

[13.13] Should I design my classes from the outside (interfaces first) or from the inside (data first)?

FAQ: Outside, of course.

A long discussion about the accessors one should have in a LinkedList class follows. A ground-breaking conclusion is reached: you should give accessors to the elements of the list, but not the "infrastructure" (say, the pointer to the first node).

FQA: What? What does this question have to do with operator overloading? And what does the kind of accessors you want to have in your class (the final code) have to do with the way you design it (think before you code)?

There's no single good way to design classes or any kind of software. Sometimes the interfaces are the important thing, sometimes the implementation, and sometimes both. Designing is a kind of thinking. Sometimes you need to think about the data in order to figure out what kind of interfaces are ultimately possible to implement efficiently. Sometimes you must figure out the interface first to make sure the whole thing is actually useful. Frequently you have to think about both and possibly iteratively refine them.

The idea that implementations are not important (because you can always change them or for other reasons) may only emerge from implementing boringly trivial things, or from doing nothing (one way of doing nothing in the software industry is to obsessively seek the best way to spell the iteration over the elements of a list for a living).

By the way, the FAQ mentions "a billion-line app". C++ doesn't scale to many lines of code in the sense that a large monolithic C++ application (one where all code runs in a single address space) can hardly be maintainable. This claim may be made about other languages as well, but the problem is especially severe with languages assuming unmanaged environments, where a buffer overflow in module A may silently corrupt the data of module B. If you have lots of unmanaged code, breaking it into processes can save several tons of your bacon (of course you have to build the system from the beginning that way - retro-fitting it is extremely expensive).

[13.14] How can I overload the prefix and postfix forms of operators ++ and --?

FAQ: Like this:

Iter& operator++ ();    // ++p
Iter  operator++ (int); // p++

The postfix version should return something equivalent to the copy of *this made before the operator call.

FQA: Silly syntax, isn't it? If you do overload operators, please read and follow all the boring rules about their syntax and semantics. This will help you soften the blow on your users, or even fall asleep and forget about the whole thing if we are lucky.

[13.15] Which is more efficient: i++ or ++i?

FAQ: It's the same for built-in types. ++i may be faster for user-defined types, because there's no need to create a copy that the compiler may fail to optimize out. Most likely the overhead is small, but why not pick the habit of using ++i?

FQA: Oh, suddenly the all-mighty compiler can't optimize out a temporary! I actually picked that habit at some point. That was before I picked the even more useful habit of avoiding overloaded C++ operators.

The really interesting questions are about the performance of A+B*C compared to D=B;D*=C;D+=A compared to an optimized multiply_and_add_matrices(A,B,C) function.

Copyright © 2007-2009 Yossi Kreinin
revised 17 October 2009