Getting Along With The Comma Operator in C++
The comma operator is a curious operator and seldom used, but it happens to encounter it in code. And sometimes by mistake. Such encounters can give a hard time understanding the code.
For this reason it is useful to know what it does, and what it doesn’t do.
This article is not made to show how to put operator,
at the center of your designs, but rather help you get along with it when you find it in code. At some point this will save you some question marks popping up above your head when you read code.
A comma operator?
Yes, there is such as thing as operator,
in C++, just as much as there is operator+
or operator*
.
It has a built-in implementation on all combination of two types, that does the following:
- first evaluate the left hand side expression,
- then evaluate the right hand side expression,
- finally return the result of the evaluation of the right hand side expression.
For example consider the following expression:
f(), g()
with f and g being two functions. Here the compiler calls f, then calls g, then returns the value returned by g.
Note that even though the return value of f is discarded (meaning that it isn’t used) like any other temporary object it persists until the end of the execution of the enclosing statement.
operator,
is for example seen in the for-loop expressions that maintain several counter variables:
for (...; ...; ++i, ++j)
Don’t overload it (in particular before C++17)
Like its operator siblings, operator,
can be overloaded for a particular combination of two types.
But as Scott Meyers explains in Item 7 of More Effective C++, you don’t want to do that. The reason is that when you overload an operator it is considered like a normal function in terms of order of evaluation. That is, the order of evaluation is unspecified.
And for the operators &&
, ||
and ,
the order of evaluation is part of their semantics. Changing this order kind of breaks those semantics and makes the code even more confusing than the existence of a comma operator in the first place.
This has changed in C++17. Indeed, in C++17 the order of custom operators &&
, ||
and ,
is the same as the one of built-in types (so left hand side first). So in theory you could more easily override operator,
in C++17. But I have yet to see a case where it makes sense to overload operator,
.
Note that even with the order of evaluation of C++17, you still wouldn’t want to override operator&&
and operator||
, because their built-in versions have a short-circuit behaviour: they don’t evaluate the second paramater if the first one evaluates to false
(resp. true
) for operator&&
(resp. operator||
). And the custom versions don’t have this short-circuiting behaviour, even in C++17.
Code that doesn’t look like what it does
Here is an interesting case that was pointed out to me by my colleague Aadam. Thanks for bringing this up Aadam! I’ve replaced all domain types and values with int
s for this example:
int sum(int x, int y) { return x + y; } int main() { int x = 4; int y = 0; int z = 0; z = sum(x, y); std::cout << z << '\n'; }
Can you predict the output of this code?
4
Wasn’t hard, right?
Now an unfortunate refactoring introduces a typo in the code: note how the call to the function sum
has been removed but the second parameter was left by mistake:
int sum(int x, int y) { return x + y; } int main() { int x = 4; int y = 0; int z = 0; z = x, y; std::cout << z << '\n'; }
Now can you predict the output of this code?
4
You read it right, it is 4, not 0.
This baffled us: this comma operator should return the right hand side value, so 0, right?
The explanation lies in the precedence rules of operators: comma is the last operator in terms of precedence. So in particular it comes after… operator=
! Therefore, the assignement statement on z should be read z=x
, and only after thisoperator,
takes the result of that and y
.
With all this, you’re now more equipped to deal with this curious operator next time you encounter it.
Want to share a story that happened to you with operator,
too?
Share this post!