86

I just read one of Joel's articles in which he says:

In general, I have to admit that I’m a little bit scared of language features that hide things. When you see the code

i = j * 5;

… in C you know, at least, that j is being multiplied by five and the results stored in i.

But if you see that same snippet of code in C++, you don’t know anything. Nothing. The only way to know what’s really happening in C++ is to find out what types i and j are, something which might be declared somewhere altogether else. That’s because j might be of a type that has operator* overloaded and it does something terribly witty when you try to multiply it.

(Emphasis mine.) Scared of language features that hide things? How can you be scared of that? Isn't hiding things (also known as abstraction) one of the key ideas of object-oriented programming? Everytime you call a method a.foo(b), you don't have any idea what that might do. You have to find out what types a and b are, something which might be declared somewhere altogether else. So should we do away with object-oriented programming, because it hides too much things from the programmer?

And how is j * 5 any different from j.multiply(5), which you might have to write in a language that does not support operator overloading? Again, you would have to find out the type of j and peek inside the multiply method, because lo and behold, j might be of a type that has a multiply method that does something terribly witty.

"Muahaha, I'm an evil programmer that names a method multiply, but what it actually does is totally obscure and non-intuitive and has absolutely nothing to do whatsoever with multiplying things." Is that a scenario we must take into consideration when designing a programming language? Then we have to abandon identifiers from programming languages on the grounds that they might be misleading!

If you want to know what a method does, you can either glance at the documentation or peek inside the implementation. Operator overloading is just syntactic sugar, and I don't see how it changes the game at all.

Please enlighten me.

9
  • 21
    +1: Well written, well argued, interesting topic and highly debatable. A shining example of a p.se question. Dec 10, 2010 at 12:37
  • 19
    +1: People listen to Joel Spolsky because he writes well and is well-known. But that doesnt make him right 100% of the time. I agree with your argument. If we all followed Joel's logic here, we'd never get anywhere.
    – Nobody
    Dec 10, 2010 at 12:59
  • 5
    I'd argue that either i and j are declared locally so you can see their type quickly, or they're sucky variable names and should be renamed appropriately. Dec 10, 2010 at 13:40
  • 5
    +1, but don't forget the best part of Joel's article: after running a marathon toward the correct answer, he for no apparent reason stops 50 feet short of it. Wrong code shouldn't just look wrong; it shouldn't compile. Dec 10, 2010 at 15:57
  • 3
    @Larry: You can make wrong code fail to compile by defining classes appropriately, so in his example you could have SafeString and UnsafeString in C++, or RowIndex and ColumnIndex, but you'd then have to use operator overloading to make them behave intuitively. Dec 10, 2010 at 16:26

15 Answers 15

34

Abstraction 'hides' code so you don't have to be concerned about the inner workings and often so you can't change them, but the intention was not to prevent you from looking at it. We just make assumptions about operators and like Joel said, it could be anywhere. Having a programming feature requiring all overloaded operators to be established in a specific location may help to find it, but I'm not sure it makes using it any easier.

I don't see making * do something that doesn't closely resemble multiplication any better than a function called Get_Some_Data that deletes data.

15
  • 14
    +1 For the 'I don't see' bit. Language features are there for use, not abuse.
    – Michael K
    Dec 10, 2010 at 13:37
  • 5
    Yet we have a << operator defined on streams which has nothing to do with the bitwise shift, right in the standard library of C++.
    – Malcolm
    Jul 25, 2013 at 16:32
  • the 'bitwise shift' operator is only called that for historical reasons. When applied to standard types, it does a bitwise shift (in the same way that the + operator adds numbers together when applied to numeric types), however when applied to a complex type, it can do what it likes, as long as it makes sense for that type.
    – gbjbaanb
    Apr 7, 2014 at 9:33
  • 1
    * is also used for dereferencing (as done by smart pointers and iterators); it's not clear where to put the boundary between good and bad overloading Feb 9, 2015 at 12:57
  • It wouldn't be just anywhere, it'd be in the type definition of j.
    – Andy
    Oct 29, 2015 at 1:07
22

IMHO, language features such as operator overloading give the programmer more power. And, as we all know, with great power comes great responsibility. Features that give you more power also give you more ways to shoot yourself in the foot, and, obviously, should be used judiciously.

For example, it makes perfect sense to overload the + or the * operator for class Matrix or class Complex. Everyone will instantly know what it means. On the other hand, to me the fact that + means concatenation of strings is not at all obvious, even though Java does this as a part of the language, and STL does for std::string using operator overloading.

Another good example of when operator overloading makes code more clear is smart pointers in C++. You want the smart pointers to behave like regular pointers as much as possible, so it makes perfect sense to overload the unary * and -> operators.

In essence, operator overloading is nothing more than just another way to name a function. And there is a rule for naming functions: the name must be descriptive, making it immediately obvious what the function does. The same exact rule applies to operator overloading.

9
  • 1
    Your last two sentences get to the heart of the objection to operator overloading: the desire for all code to be immediately obvious. Dec 10, 2010 at 16:00
  • 2
    Isn't it obvious what M * N means, where M and N are of type Matrix?
    – Dima
    Dec 10, 2010 at 16:14
  • 2
    @Fred: Nope. There is one kind of matrix multiplication. You can multiply an m x n matrix by an n x k matrix and get an m x k matrix.
    – Dima
    Dec 10, 2010 at 18:35
  • 1
    @FredOverflow: There are different ways to multiply a three-dimensional vector, one giving you a scalar and one giving you another three-dimensional vector, and so overloading * for those can cause confusion. Arguably you could use operator*() for the dot product and operator%() for the cross product, but I wouldn't do that for a general-use library. Dec 10, 2010 at 18:48
  • 2
    @Martin Beckett: No. C++ isn't allowed to reorder A-B as B-A either, and all operators follow that pattern. Although there is always one exception: when the compiler can prove it doesn't matter , it's allowed to rearrange everything.
    – Sjoerd
    Apr 10, 2011 at 7:13
9

In Haskell "+", "-", "*", "/" etc are just (infix) functions.

Should you name an infix function "plus" as in "4 plus 2"? Why not, if addition is what your function does. Should you name your "plus" function "+"? Why not.

I think the issue with so called "operators" are, that they mostly resemble mathematical operations and there are not many ways to interpret those and thus there are high expectations about what such a method/function/operator does.

EDIT: made my point more clear

6
  • Erm, except for what's inherited from C, C++ (and that's what Fred was asking about) does pretty much the same thing. Now what's your take on whether this is good or bad?
    – sbi
    Dec 10, 2010 at 12:39
  • @sbi I love operator overloading... Actually even C has overloaded operators... You can use them for int, float, long long and whatever. So what's that all about?
    – FUZxxl
    Oct 8, 2011 at 14:04
  • @FUZxxl: This is all about user-defined operators overloading the built-in ones.
    – sbi
    Oct 8, 2011 at 14:12
  • 1
    @sbi Haskell has no distinction between builtin and user-defined. All operators are equal. You can even turn some extensions on that remove all predefined stuff and let you write anything from scratch, including any operators.
    – FUZxxl
    Oct 8, 2011 at 14:18
  • @FUZxxl: That might well be, but those opposing overloaded operators usually do not oppose using built-in + for different built-in number types, but creating user-defined overloads. hence my comment.
    – sbi
    Oct 8, 2011 at 14:23
8

Based on the other answers I've seen, I can only conclude that the real objection to operator overloading is the desire for immediately obvious code.

This is tragic for two reasons:

  1. Carried to its logical conclusion, the principle that code should be immediately obvious would have us all still coding in COBOL.
  2. You don't learn from code that is immediately obvious. You learn from code that makes sense once you take some time to think about how it works.
1
  • Learning from code is not always the primary objective though. In a case like "feature X is glitching, the person who wrote it left the company, and you need to fix it ASAP", I'd much rather have code that's immediately obvious.
    – Errorsatz
    May 24, 2019 at 19:01
5

I somewhat agree.

If you write multiply(j,5), j could be of a scalar or matrix type, making multiply() more or less complex, depending on what j is. However, if you abandon the idea of overloading altogether, then the function would have to be named multiply_scalar() or multiply_matrix() which would make it obvious what's happening underneath.

There's code where many of us would prefer it one way and there's code where most of us would prefer it the other way. Most of the code, however, falls into thew middle ground between those two extremes. What you prefer there depends on your background and personal preferences.

9
  • Good point. However, abandoning overloading altogether does not play nice with generic programming... Dec 10, 2010 at 12:20
  • @FredO: Of course not. But generic programming is all about using the same algorithm for very different types, so those preferring multiply_matrix() won't like generic programming either.
    – sbi
    Dec 10, 2010 at 13:04
  • 2
    You're rather optimistic about names aren't you? Based on some places I've worked, I'd expect names like 'multiply()` and 'multiplym()` or maybe real_multiply() or so. Developers often aren't good with names, and operator*() at least is going to be consistent. Dec 10, 2010 at 18:56
  • @David: Yeah, I skipped over the fact that the names might be bad. But then we might just as well assume that operator*() might do something stupid, j is a macro evaluating to an expressions involving five function calls, and whatnot. You then can't compare the two approaches anymore then. But, yes, naming things well is hard, although well worth whatever time it takes.
    – sbi
    Dec 10, 2010 at 19:21
  • 5
    @David: And since naming things is hard, names should be banished from programming languages, right? It's just too easy to get them wrong! ;-) Dec 11, 2010 at 8:04
5

I see two problems with operator overloading.

  1. Overloading changes the semantics of the operator, even if that is not intended by the programmer. For example, when you overload &&, || or ,, you lose the sequence points that are implied by the built-in variants of these operators (as well as the short-circuiting behaviour of the logical operators). For this reason, it is better not to overload these operators, even if the language allows it.
  2. Some people see operator overloading as such a nice feature, they start to use it everywhere, even if it is not the appropriate solution. This causes other people to over-react in the other direction and warn against the use of operator overloading. I don't agree with either group, but take the middle ground: Operator overloading should be used sparingly and only when
    • the overloaded operator has the natural meaning for both the domain experts and the software experts. If those two groups do not agree on the natural meaning for the operator, don't overload it.
    • for the type(s) involved, there is no natural meaning for the operator and the immediate context (preferably same expression, but no more than a few lines) always makes it clear what the meaning is of the operator. An example of this category would be operator<< for streams.
4
  • 2
    +1 from me, but the second argument can equally well be applied to inheritance. Many people don't have a clue about inheritance and try to apply it to everything. I think most programmers would agree that it is possible to misuse inheritance. Does that mean inheritance is "evil" and should be abandoned from programming languages? Or should we leave it in because it can also be useful? Dec 10, 2010 at 17:42
  • @FredOverflow The second argument can be applied to anything that is "new and hot". I am not giving it as an argument to remove operator overloading from a language, but as a reason why people argue against it. As far as I am concerned, operator overloading is useful but should be used with care. Dec 14, 2010 at 10:50
  • IMHO, allowing overloads of && and || in a way which does not imply sequencing was a big mistake (IMHO, if C++ was going to allow overloading of those, it should have used a special "two-function" format, with the first function being required to return a type which was implicitly convertible to an integer; the second function could take two or three arguments, with the "extra" argument of the second function being the return type of the first. The compiler would call the first function and then, if it returned non-zero, evaluate the second operand and call the second function upon it.)
    – supercat
    Jul 19, 2012 at 15:54
  • Of course, that's not nearly as bizarre as allowing the comma operator to be overloaded. Incidentally, one overloading-ish thing I've not really seen, but would like to, would be a means of tight-binding member access, allowing an expression like foo.bar[3].X to be handled by foo's class, rather than requiring foo to expose a member which could support subscripting and then expose a member X. If one wanted to force evaluation via actual member access, one would write ((foo.bar)[3]).X.
    – supercat
    Jul 19, 2012 at 16:02
3

Based on my personal experience, the Java way of allowing multiple methods, but not overloading operator, means that whenever you see an operator you know exactly what it does.

You do not have to see if * invokes strange code but know that it is a multiply, and it behaves exactly like in the way defined by the Java Language Specification. This means you can concentrate on the actual behaviour instead of finding out all the wicket stuff defined by the programmer.

In other words, prohibiting operator overload is a benefit to the reader, not the writer, and hence makes programs easier to maintain!

13
  • +1, with a caveat: C++ gives you enough rope to hang yourself. But if I want to implement a linked list in C++, I'd want the ability to use [] to access the nth element. It makes sense to use the operators for data that (mathematically speaking) they are valid for.
    – Michael K
    Dec 10, 2010 at 13:41
  • @Michael, you cannot live with the list.get(n) syntax?
    – user1249
    Dec 10, 2010 at 13:44
  • @Thorbjørn: It's actually fine as well, perhaps a poor example. Time may be better - overloading +,- would make sense rather than time.add(anotherTime).
    – Michael K
    Dec 10, 2010 at 13:50
  • 4
    @Michael: About linked lists, std::list does not overload operator[] (or give any other means of indexing into the list), because such an operation would be O(n), and a list interface should not expose such a function if you care about efficiency. Clients might be tempted to iterate over linked lists with indexes, making O(n) algorithms needlessly O(n^2). You see that quite often in Java code, especially if people work with the List interface which aims to abstract complexity away completely. Dec 10, 2010 at 17:23
  • 5
    @Thor: "But in order to be certain you have to check :)"... Again, this is not tied to operator overloading. If you see time.add(anotherTime), you will also have to check if the library programmer implemented the add operation "correctly" (whatever that means). Dec 10, 2010 at 17:35
3

One difference between overloading a * b and calling multiply(a,b) is that the latter can easily be grepped for. If the multiply function isn't overloaded for different types then you can find out exactly what the function is going to do, without having to track through the types of a and b.

Linus Torvalds has an interesting argument about operator overloading. In something like linux kernel development, where most of the changes are sent via patches over email, it's important that the maintainers can understand what a patch will do with only a few lines of context around each change. If functions and operators are not overloaded then the patch can more easily be read in a context independent way, as you don't have to go through the changed file working out what all of the types are and check for overloaded operators.

9
  • Isn't the linux kernel developed in pure C? Why discuss (operator) overloading at all in this context? Dec 10, 2010 at 13:26
  • The concerns are the same for any project with a similar development process, regardless of language. Excessive overloading can make it difficult to understand the impact of changes if all you have to go on are a few lines from a patch file. Dec 10, 2010 at 13:34
  • @FredOverflow: The Linux kernel is in GCC C really. It uses all sorts of extensions that give its C an almost C++ feel at some times. I'm thinking of some of the fancy type manipulations.
    – Zan Lynx
    Dec 10, 2010 at 17:39
  • 2
    @Scott: There is no point in discussing the "evilness" of overloading with respect to projects programmed in C, because C does not have the ability to overload functions. Dec 10, 2010 at 17:47
  • 4
    Linus Torvalds seems to me to have a narrow viewpoint. He sometimes criticizes things that aren't really useful for Linux kernel programming as if that makes them unsuitable for general use. Subversion is one example. It's a nice VCS, but Linux kernel development really needs a distributed VCS, so Linus criticized SVN in general. Dec 10, 2010 at 18:42
2

I suspect it has something to do with breaking expectations. I've you're used to C++, you're used to operator behavior not being dictated entirely by the language, and you won't be surprised when an operator does something odd. If you're used to languages that don't have that feature, and then see C++ code, you bring along the expectations from those other languages, and may be nastily surprised when you discover that an overloaded operator does something funky.

Personally I think there's a difference. When you can change the behavior of the language's built-in syntax, it becomes more opaque to reason about. Languages that don't allow meta-programming are syntactically less powerful, but conceptually simpler to understand.

1
  • Overloaded operators should never do "something odd". It's fine if it does something complex, of course. But only overload when it has one, obvious meaning.
    – Sjoerd
    Apr 10, 2011 at 7:18
2

I think that overloading math operators is not the real issue with operator overloading in C++. I think overloading operators that should not rely on the context of the expression (i.e. type) is "evil". E.g. overloading , [ ] ( ) -> ->* new delete or even the unary *. You have a certain set of expectations from those operators that should never change.

3
  • +1 Don't make [] the equivalent of ++.
    – Michael K
    Dec 10, 2010 at 13:38
  • 3
    Are you saying we shouldn't be able to overload the operators you mentioned at all ? Or are you just saying that we should overload them for sane purposes only? Because I would hate to see containers without operator[], functors without operator(), smart pointers without operator-> and so on. Dec 10, 2010 at 17:39
  • I'm saying that the potential problem of operator overloading with math operations is small compared to those operators. Doing something clever or crazy with math operators might troublesome, but the operators I listed, which people usually don't think of as operators but rather basic language elements, should always meet the expectation defined by the language. [] should always be an array-like accessor, and -> should always mean accessing a member. It doesn't matter if it's actually an array or a different container, or if it's a smart pointer or not. Dec 10, 2010 at 21:44
2

In addition to what has already been said here, there's one more argument against operator overloading. Indeed, if you write +, this is kind of obvious that you mean addition of something to something. But this is not always the case.

C++ itself provides a great example of such a case. How is stream << 1 supposed to be read? stream shifted left by 1? It is not obvious at all unless you explicitly know that << in C++ also writes to the stream. However, if this operation were implemented as a method, no sane developer would write o.leftShift(1), it would be something like o.write(1).

The bottom line is that by making operator overloading unavailable, the language makes programmers think about the names of operations. Even if the chosen name is not perfect, it is still harder to misinterpret a name than a sign.

2

I perfectly understand you do not like Joel's argument about hiding. Me neither. It's indeed much better to use '+' for things like built-in numerical types or for your own ones like, say, matrix. I admit this is neat and elegant to be able to multiply two matrices with the '*' instead of '.multiply( )'. And after all we've got the same kind of abstraction in both cases.

What hurts here is the readability of your code. In a real-life cases, not in the academic example of matrix multiplication. Especially if your language allows to define operators that are not initially present in the language core, for instance =:=. A lot of extra questions arise at this point. What is that damn operator about? I mean what is the precedence of that thing? What is the associativity? In which order is the a =:= b =:= c really executed?

That is already an argument against operator overloading. Still not convinced? Checking the precedence rules took you no more then 10 sec? Ok, let's go further.

If you start to use a language that allows operator overloading, for instance that popular one whose name begins with 'S', you will quickly learn that library designers love to override operators. Of course they are well educated, they follow the best practices (no cynicism here) and all their APIs make perfect sense when we look at them separately.

Now imagine you have to use a few APIs that make heavy use of operators overloading together in a one piece of code. Or even better - you have to read some legacy code like that. This is when the operator overloading really sucks. Basically if there is a lot of overloaded operators in one place they will soon start to mingle with the other non alpha-numerical characters in your program code. They will mingle with non alpha-numerical characters that are not really operators but rather some more fundamental language grammar elements that define things like blocks and scopes, shape flow control statements or denote some meta thingies. You will need to put the glasses and move your eyes 10 cm closer to the LCD display to understand that visual mess.

1
  • 1
    Overloading existing operators and inventing new operators are not the same thing, but +1 from me. Apr 16, 2015 at 17:32
1

In general, I avoid using operator overloading in non-intuitive ways. That is, if I have a numeric class, overloading * is acceptable (and encouraged). However, if I have a class Employee, what would overloading * do? In other words, overload operators in intuitive ways that make it easy to read and understand.

Acceptable/Encouraged:

class Complex
{
public:
    double r;
    double i;

    Complex operator*(const Compex& rhs)
    {
        Complex result;
        result.r = (r * rhs.r) - (i * rhs.i);
        result.i = (r * rhs.i) + (i * rhs.r);
        return result;
    }
};

Not acceptable:

class Employee
{
public:
    std::string name;
    std::string address;
    std::string phone_number;

    Employee operator* (const Employee& e)
    {
        // what the hell do I do here??
    }
};
1
  • 2
    Multiplying employees? Surely that's a sackable offence, if they do it on the boardroom table, that is.
    – gbjbaanb
    Oct 8, 2011 at 23:48
1

In comparison to spelled out methods, operators are shorter, but also they don't require parentheses. Parentheses are relatively inconvenient to type. And you must balance them. In total, any method call requires three characters of plain noise compared to an operator. This makes using operators very, very tempting.
Why else would anyone want to this: cout << "Hello world"?

The problem with overloading is, that most programmers are unbelievably lazy and most programmers cannot afford to be.

What drives C++ programmers to the abuse of operator overloading is the not its presence, but the absence of a neater way to perform method calls. And people are not just afraid of operator overloading because it's possible, but because it's done.
Note that for example in Ruby and Scala nobody is afraid of operator overloading. Apart from the fact, that the use of operators is not really shorter than methods, another reason is, that Ruby limits operator overloading to a sensible minimum, while Scala allows you to declare your own operators thus making avoiding collision trivial.

1
  • or, in C#, for using += to tie an event to a delegate. I don't think that blaming language features for programmer stupidity is a constructive way forward.
    – gbjbaanb
    Oct 8, 2011 at 23:50
0

The reason Operator Overloading is scary, is because there are a large number of programmers that would never even THINK that * doesn't mean simply "multiply", whereas a method like foo.multiply(bar) at least instantly points out to that programmer that someone wrote a custom multiply method. At which point they would wonder why and go investigating.

I have worked with "good programmers" who were in high level positions that would create methods called "CompareValues" that would take 2 arguments, and apply the values from one to the other and return a boolean. Or a method called "LoadTheValues" that would go to the database for 3 other objects, get values, do calculations, modify this and save it to the database.

If I am working on a team with those types of programmers, I instantly know to investigate things they have worked on. If they overloaded an operator, I have no way at all of knowing that they did it except to assume they did and go looking.

In a perfect world, or a team with perfect programmers, operator overloading is probably a fantastic tool. I have yet to work on a team of perfect programmers though, so that's why it's scary.

Not the answer you're looking for? Browse other questions tagged or ask your own question.