
The Subtle Dangers of the Comma Operator (C++) - pekalicious
https://humanreadablemag.com/issues/3/articles/the-subtle-dangers-of-the-comma-operator
======
einpoklum
> Overloading the Comma Operator Is Powerful

Clearly, it's over 9000!

... more seriously though, overloading the comma operator is a bad idea and
you shouldn't do it. In fact, almost nobody does it.

Specifically, you don't want to break the principle of least astonishment with
a command such as

    
    
        v+= 1,2,3,4,5;
    

Assuming you want to add up a bunch of literals, what you could do is
something like:

    
    
       std::array data { 1,2,3,4,5 };
       v += std::reduce(data.begin(), data.end());
    

and if you've implemented some <algorithm> and <numeric> wrappers for
containers, you would have something like an

    
    
        template<class Container>
        typename Container::value_type 
        reduce(const Container& container);
    

and then you would write

    
    
       v += reduce(std::array{ 1,2,3,4,5 } );
    

which is terse and much clearer.

~~~
jzwinck
An even more concise form is built in, and I think more obvious to readers:

    
    
        v.insert(v.end(), { 1, 2, 3, 4, 5 });

~~~
einpoklum
I was actually not describing adding elements to a container, but adding up
values.

~~~
owl57
See? That's the problem with arithmetic on containers. Python has that
already:

    
    
        v += 4, 5, 6
    

Comma doesn't do weird shit: that's just a tuple of numbers. But "+=" then
does weird shit: it means concatenation when v is a standard library list, and
element-wise addition when v is a numpy vector.

------
mehrdadn
> Once the vector is constructed, we can only add elements one by one by using
> the push_back member function.

Nope... std::vector::insert() takes an std::initializer_list [1].

[1]
[https://en.cppreference.com/w/cpp/container/vector/insert](https://en.cppreference.com/w/cpp/container/vector/insert)

------
jeffbee
The example (v += 1,2,3,4,5) is neither "nice" nor "expressive". It's
confusing and dumb. The reader of this code can't be expected to know what it
does. And, looking at the implementation, there's not even an efficiency
benefit from doing this. You'd have a more efficient and literate program with
std::iota.

The rest of the article isn't wrong, but it fails to establish why anyone
thinks this pattern is "nice".

~~~
simias
It might be a bit extreme but the more I'm exposed to operator overloading the
more I think that it's a bad idea 99% of the time.

The _only_ use that seems absolutely in my opinion is when the overload is
absolutely transparent mathematically, for instance to implement geometrical
transformations on a Matrix class.

That works because in this case you don't actually end up with "custom"
behavior, you just expand the standard and well understood notation of the
language by plugging the standard and well understood mathematical notation
for matrix operations. Anybody who understands this mathematical notation will
be able to understand what the code does without additional context.

Anything beyond that is just asking for trouble IMO. It's basically code
obfuscation.

Of course C++ has precedent for that sort of insanity, especially with the IMO
absolutely bonkers use of the bit shift operators for... input/output
processing. A notation that you'll note hasn't had a lot of success outside of
C++.

~~~
roenxi
> It might be a bit extreme but the more I'm exposed to operator overloading
> the more I think that it's a bad idea 99% of the time.

Quite a moderate position in practice. There are a substantial number of
programmers who enjoy being able to tell what basic syntax does from memory
without having to cross-reference documentation and source files.

And as you allude to, the example in question isn't a sum; it is an append. It
would be far more appropriate to have an a = append(a, {1,2,3,4,5}) style
construct. Or something with pointers if efficiency is important. Or construct
the vector inline in older versions of C++. The += should be reserved for
actual vector summing.

This whole article stands as a reminder that C++ is a remarkable language, and
is starting to make up a lot of ground on achieving feature parity with Common
Lisp.

------
indogooner
>> The first one is that if we overload the comma operator, we need to be
extra careful to cover all cases and think about lvalues and rvalues.
Otherwise we end up with buggy code.

Extra careful to cover all cases is when I start looking at alternatives. In
this case it is not even enhancing readability. Even if you may be an
excellent programmer think whether your team can be that extra careful most of
the times before introducing these quirks in your code.

~~~
Someone
_“Extra careful to cover all cases is when I start looking at alternatives.“_

IMO, an advantage of C++ is that, in well-written code, being extra careful is
(mostly) limited to the implementer of code, not to its users.

It would be nice if nobody would have to be extra careful, but that’s the
price you pay for power, I fear.

~~~
pornel
Rust shows that you can remove most of the footguns, and keep the power with a
much smaller "extra careful" surface.

For example, instead of a comma operator and its overloads, you can have
tuples. Then `let (a,b) = (b,a)` works as expected. If you want weird code,
you can overload `vec += (1,2,3,4,5)` with a tuple, and that's a
straightforward case with no hidden gotchas.

------
jzwinck
Boost Assign never seemed really useful to me. More like "Here's a zany thing
you can do in C++."

Subsequent to the birth of Boost Assign, the language improved:

    
    
        for (int i : {1,2,3,4,5})
            v.push_back(i);
    

Simple, built in, anyone can understand it.

------
Asooka
In my life as a professional c++ programmer, overloading the comma operator
has been much less useful than using goto. The operator itself is fine and
using it in its default behaviour can be good, but overloading it is
absolutely useless.

------
robalni
I would rather call this "the subtle dangers of function overloading".

One thing I like about the C (not ++) language is that it's always clear what
function that is being called by looking at the function name. In C++ you have
to guess a lot of times (or look very carefully).

~~~
lmm
Nah. The problem is that , doesn't look like a function, isn't treated as a
function by most of your tooling, and so it's very surprising if it behaves
like one (plus it's not at all clear what you'd expect a function called , to
do in most cases). Polymorphic functions are fine when they have sensible
names and are understood as functions by the reader and tooling (of course it
helps if your language is actually parseable by those tools, which C++ isn't).

~~~
robalni
I don't think the main problem in this article is that "," doesn't look like a
function because we are told that it is overloaded so we know it's a function.

The problem is that the behavior of the code silently changes when a function
is moved and this is because of the overloading feature in C++. You can get
those types of problems in other cases too when you overload functions that
have a good name, but maybe it's less likely since I guess the comma operator
is already defined for all different types but named functions have to be
defined by the programmer.

~~~
lmm
> I don't think the main problem in this article is that "," doesn't look like
> a function because we are told that it is overloaded so we know it's a
> function.

Well it's a function in one of the code snippets and not in the other. That it
can be both is definitely part of the problem.

> You can get those types of problems in other cases too when you overload
> functions that have a good name, but maybe it's less likely since I guess
> the comma operator is already defined for all different types but named
> functions have to be defined by the programmer.

The problem is that it's not just overloaded but overridden. Subtyping is
problematic at the best of times, but the subtyping relationship of C++
lvalues and rvalues is particularly insidious since it is completely invisible
in the code.

------
leni536
The built-in comma operator has strict sequencing semantics:

Every value computation and side effect of the first (left) argument of the
_built-in_ comma operator , is sequenced before every value computation and
side effect of the second (right) argument. [1]

The same sequencing guarantee is not applied to a user-defined comma operator.

For the similar reasons it's not a great idea to overload && and || either.

[1]
[https://en.cppreference.com/w/cpp/language/eval_order](https://en.cppreference.com/w/cpp/language/eval_order)
(point 9)

