Remix.run Logo
butterisgood 5 days ago

Overloaded operators were a terrible mistake in every programming language I've encountered them in. (Yes, sorry Haskell, you too!)

I don't think move semantics are really that bad personally, and some languages move by default (isn't that Rust's whole thing?).

What I don't like is the implicit ambiguous nature of "What does this line of code mean out of context" in C++. Good luck!

I have hope for C++front/Cpp2. https://github.com/hsutter/cppfront

(oh and I think you can write a whole book on the different ways to initialize variables in C++).

The result is you might be able to use C++ to write something new, and stick to a style that's readable... to you! But it might not make everyone else who "knows C++" instantly able to work on your code.

wvenable 5 days ago | parent | next [-]

Overloaded operators are great. But overloaded operators that do something entirely different than their intended purpose is bad. So a + operator that does an add in your custom numeric data type is good. But using << for output is bad.

ecshafer 5 days ago | parent | next [-]

The first programming language that used overloaded operators I really got into was Scala, and I still love it. I love that instead of Java's x.add(y); I can overload + so that it calls .add when between two objects of type a. It of course has to be used responsibly, but it makes a lot of code really more readable.

lelanthran 4 days ago | parent | next [-]

> The first programming language that used overloaded operators I really got into was Scala, and I still love it. I love that instead of Java's x.add(y); I can overload + so that it calls .add when between two objects of type a. It of course has to be used responsibly, but it makes a lot of code really more readable.

The problem, for me, with overloaded operators in something like C++ is that it frequently feels like an afterthought.

Doing "overloaded operators" in Lisp (CLOS + MOP) has much better "vibes" to me than doing overloaded operators in C++ or Scala.

Attrecomet 4 days ago | parent | prev [-]

Exactly, not allowing operator overload leads to Java hell, where we need verbose functions for calls that should be '+' or similar.

LexiMax 5 days ago | parent | prev | next [-]

I will die on the hill that string concatenation should have its own operator, and overloading + for the operation is a mistake.

Languages that get it right: SQL, Lua, ML, Perl, PHP, Visual Basic.

o11c 5 days ago | parent | next [-]

I think it's fine when the language has sufficiently strict types for string concatenation.

Unfortunately, many languages allow `string + int`, which is quite problematic. Java is to blame for some of this.

And C++ is even worse since literals are `const char[]` which decays to pointer.

Languages okay by my standard but not yours include: Python, Ruby.

zevets 5 days ago | parent | prev | next [-]

Alternatively, any implementation of operator+ should have a notional identity element, an inverse element and be commutative.

AlotOfReading 5 days ago | parent [-]

C++ would be a very different language if you couldn't use floats:

(NaN + 0.0) != 0.0 + NaN

Inf + -Inf != Inf

I suspect the algebraists would also be pissed if you took away their overloads for hypercomplex numbers and other exotic objects.

112233 5 days ago | parent | prev | next [-]

Tangential, but Lua is the most write-only language I have had pleasure working with. The implementation and language design are 12 out of 10, top class. But once you need to read someone else's code, and they use overloads liberally to implement MCP and OODB and stuff, all in one codebase, and you have no idea if "." will index table, launch Voyager, or dump core, because everything is dispatched at runtime, it's panic followed by ennui.

butterisgood 4 days ago | parent [-]

Perl was one for me that I always had a little trouble reading again later.

I guess forth as well... hmmm

Animats 5 days ago | parent | prev | next [-]

> string concatenation should have its own operator,

It does: |

That character was put in ASCII specifically for concatenation in PL/1.

Then came C.

renox 5 days ago | parent [-]

D (as always) is clever: the operator is ~ So no confusion between addition and concatenation and you can keep | for or.

Defletter 5 days ago | parent [-]

Question, does that work with other types? Say you have two u16 values, can you concatenate them together with ~ into a u32 without any shifting?

nicwilson 5 days ago | parent | next [-]

It works with arrays (both fixed size, and dynamically sized) and arrays; between arrays and elements; but not between two scalar types that don't overload opBinary!"~", so no it won't work between two `ushorts` to produce a `uint`

renox 4 days ago | parent | prev [-]

No, it doesn't. But I'm not sure that this matter, a sufficiently "smart" compiler understand that this is the same thing.

account42 5 days ago | parent | prev | next [-]

Those languages need a dedicated operator because they are loosely typed which would make it ambiguous like + in JavaScript.

But C++ doesn't have that problem. Sure, a separate operator would have been cleaner (but | is already used for bitwise or) but I have never seen any bug that resulted from it and have never felt it to be an issue when writing code myself.

_flux 4 days ago | parent [-]

Though then you can have code like "hello" + "world" that doesn't compile and "hello" + 10 that will do something completely different. In some situations you could actually end up with that by gradual modification of the original code..

Granted this is probably a novice-level problem.

dgb23 5 days ago | parent | prev | next [-]

PHP overloads operators in other ways though.

CyberDildonics 4 days ago | parent | prev | next [-]

But why, where does it become a problem?

paulddraper 5 days ago | parent | prev [-]

This is so sad obvious it’s painful.

Arithmetic addition and sequence concatenation are very very different.

——

Scala got this right as well (except strings, Java holdover)

Concatenation is ++

Animats 5 days ago | parent [-]

Python managed to totally confuse this. "+" for built-in arrays is concatenation. "+" for NumPy arrays is elementwise addition. Some functions accept both types. That can end badly.

zbentley 4 days ago | parent | prev | next [-]

Regrettably, “intended purpose” is highly subjective.

Sure, << for stream output is pretty unintuitive and silly. But what about pipes for function chaining/composition (many languages overload thus), or overriding call to do e.g. HTML element wrapping, or overriding * for matrices multiplied by simple ints/vectors?

Reasonable minds can and do differ about where the line is in many of those cases. And because of that variability of interpretation, we get extremely hard to understand code. As much as I have seen value in overloading at times, I’m forced to agree that it should probably not exist entirely.

AnimalMuppet 4 days ago | parent | next [-]

Depends on what you're trying to understand.

Let's say I have matrices, and I've overloaded * for multiplying a matrix by a matrix, and a matrix by a vector, and a matrix by a number. And now I write

  a = b * c;
If I'm trying to understand this as one of a series of steps of linear algebra that I'm trying to make sure are right, that is far more comprehensible than

  a = mat_mult(b,c);
because it uses math notation, and that's closer to the way linear algebra is written.

But if I take the exact same line and try to understand exactly which functions get called, because I'm worried about numerical stability or performance or something, then the first approach hides the details and the second one is easier to understand.

This is always the way it goes with abstraction. Abstraction hides the details, so we can think at a higher level. And that's good, when you're trying to think at the higher level. When you're not, then abstraction just hides what you're really trying to understand.

fluorinerocket 4 days ago | parent [-]

but is it element by element multiplication, or matrix multiplication? I honestly just prefer calling matmul.

wvenable 4 days ago | parent | prev [-]

> we get extremely hard to understand code

The thing is code without operator overloading is also hard to understand because you might have this math thing (BigIntegers, Matrices) and you can't use standard notation.

powerboat9 3 days ago | parent | prev | next [-]

Why is using << for output bad? From what I've seen, it's usually not too hard to figure out that the left hand side is some kind of stream and not a number.

butterisgood 4 days ago | parent | prev | next [-]

I kind of like that OCaml, which I've also not used a great deal, has different operators for adding floats vs integers etc...

Extremely clear at the "call site" what's going on.

jcelerier 5 days ago | parent | prev [-]

If you've done any university-level maths you should have seen the + sign used in many other contexts than adding numbers, why should that be a problem when programming?

account42 5 days ago | parent [-]

There is usually another operator used for concatenation in math though: | or || or ⊕

The first two are already used for bitwise and logical or and the third isn't available in ASCII so I still think overloading + was a reasonable choice and doesn't cause any actual problems IME.

loeg 5 days ago | parent | prev | next [-]

> I don't think move semantics are really that bad personally, and some languages move by default (isn't that Rust's whole thing?).

Rust's move semantics are good! C++'s have a lot of non-obvious footguns.

> (oh and I think you can write a whole book on the different ways to initialize variables in C++).

Yeah. Default init vs value init, etc. Lots of footguns.

tialaramex 4 days ago | parent | prev | next [-]

So, what programmers wanted (yes, already before C++ got this) was what are called "destructive move semantics".

These assignment semantics work how real life works. If I give you this Rubik's Cube now you have the Rubik's Cube and I do not have it any more. This unlocks important optimisations for non-trivial objects which have associated resources, if I can give you a Rubik's Cube then we don't need to clone mine, give you the clone and then destroy my original which is potentially much more work.

C++ 98 didn't have such semantics, and it had this property called RAII which means when a local variable leaves scope we destroy any values in that variable. So if I have a block of code which makes a local Rubik's Cube and then the block ends the Rubik's Cube is destroyed, I wrote no code to do that it just happens.

Thus for compatibility, C++ got this terrible "C++ move" where when I give you a Rubik's Cube, I also make a new hollow Rubik's Cube which exists just to say "I'm not really a Rubik's Cube, sorry, that's gone" and this way, when the local variable goes out of scope the destruction code says "Oh, it's not really a Rubik's Cube, no need to do more work".

Yes, there is a whole book about initialization in C++: https://www.cppstories.com/2023/init-story-print/

For trivial objects, moving is not an improvement, the CPU can do less work if we just copy the object, and it may be easier to write code which doesn't act as though they were moved when in fact they were not - this is obviously true for say an integer, and hopefully you can see it will work out better for say an IPv6 address, but it's often better for even larger objects in some cases. Rust has a Copy marker trait to say "No, we don't need to move this type".

AnimalMuppet 4 days ago | parent [-]

In particular, move is important if there is something like a unique_ptr. To make a copy, I have to make a deep copy of whatever the unique_ptr points to, which could be very expensive. To do a move, I just copy the bits of the unique_ptr, but now the original object can't be the one that owns what's pointed to.

tialaramex 4 days ago | parent [-]

Sure. Notice std::unique_ptr<T> is roughly equivalent to Rust's Option<Box<T>>

The C++ "move" is basically Rust's core::mem::take - we don't just move the T from inside our box, we have to also replace it, in this case with the default, None, and in C++ our std::unique_ptr now has no object inside it.

But while Rust can carefully move things which don't have a default, C++ has to have some "hollow" moved-from state because it doesn't have destructive move.

m-schuetz 5 days ago | parent | prev | next [-]

Operator overloarding is essential for computer graphics libraries for vector and matrix multiplication, which becomes an illegible mess without.

lifthrasiir 5 days ago | parent [-]

I personally think that operator overloading itself is justified, but the pervasive scope of operator overloading is bad. To me the best solution is from OCaml: all operators are regular functions (`a + b` is `(+) a b`) and default bindings can't be changed but you can import them locally, like `let (+) = my_add in ...`. OCaml also comes with a great convenience syntax where `MyOps.(a + b * c)` is `MyOps.(+) a (MyOps.(*) b c)` (assuming that MyOps defines both `(+)` and `(*)`), which scopes operator overloading in a clear and still convenient way.

jandrewrogers 5 days ago | parent | prev [-]

A benefit of operator overloads is that you can design drop-in replacements for primitive types to which those operators apply but with stronger safety guarantees e.g. fully defining their behavior instead of leaving it up to the compiler.

This wasn't possible when they were added to the language and wasn't really transparent until C++17 or so but it has grown to be a useful safety feature.

tialaramex 4 days ago | parent [-]

However C++ offers several overloads where you don't get to provide a drop-in replacement.

Take the short-circuiting boolean operators || and &&. You can overload these in C++ but you shouldn't because the overloaded versions silently lose short-circuiting. Bjarne just didn't have a nice way to write that so, it's not provided.

So while the expression `foo(a) && bar(b)` won't execute function bar [when foo is "falsy"] if these functions just return an ordinary type which doesn't have the overloading, if they do enable overloading both functions are always executed then the results given to the overloading function.

Edited:: Numerous tweaks because apparently I can't boolean today.