Posted on • Originally published atcppsenioreas.wordpress.com on
It’s just ‘,’ – The Comma Operator
We all know that every ‘,’ matters in this language, so I decided to talk directly about that character today. So, how much impact can be for such a small little character?
The Comma Operator
This operator comes from C, where it tells the compiler to evaluate all the expressions (left to right) and to return the result of the latest evaluated expression. For example:
inta,b;a=5,b=4,b+=a,++a,std::cout<<b<<" "<<a;// Prints 9 6
Another example of that operator usage is as follows:
for(size_ti=0,k=500;i<10;++i,++k){/*...*/}
We can see this operator in action in the third section of thefor
statement. It evaluates the++i
and then evaluates++k
.
Not Every Comma is The Comma Operator
Although it might be confusing, not every comma we see in our code uses the comma operator. For example, when we pass parameters into functions, define the arguments we want to pass to the template or declare multiple objects using commas, these cases do not use the comma operator. Moreover, in the cases of passing arguments to functions, the evaluation order is not defined. Examples:
template<typenameT,typenameV>voidfunc(int,int,int);inta=5,b=6,c=7;// Not the comma operatorfor(inti=1,j=2;i<5;++i);// Not the comma operatorfunc<int,float>(a,b,c);// Not the comma operator
At this point, some of you might think there is no reason to be afraid of such an operator, if all it does is evaluate expressions left to right. Well, we are talking about C++ here, and I think it’s time to move to the spooky sides of this operator.
Fold Expressions
Another known usage of the comma operator is as part offold expressions (Since C++17). To apply multiple unrelated expressions on the variadic parameters, we can use the comma operator as in the following example:
template<typename...ArgsF>voidcall_functions(ArgsF&&...argsf){(argsf(),...);}/*...*/call_functions([]{std::cout<<"Func1\n";},[]{std::cout<<"Func2\n";},[]{std::cout<<"Func3\n";});
The ‘,’ is out there
About a year ago someone published onReddit the following example:
for(inti=0;i<10,000;i+=1)
At first glance, there is nothing wrong with this code, and yet this loop will perform 0 iterations.
The comma operator is attached between the following expressions:i < 10
and000
. Therefore it performs the left expression and evaluates true and then performs the right expression, evaluates false, and returns the latter.
Let’s look for another example:
intx=5,y=6,z;z=(x,y);
In this example, the two expressions arex
andy
. The compiler first evaluatesx
and theny
and returns the latter. But what would happen if we remove the parentheses?
intx=5,y=6,z;z=x,y;
Well, this time the expressions arez = x
andy
. The exact opposite of the previous example.
Dangerous Yesterday, Powerful Today
Before C++23, subscript expression (operator[]
) could accept only a single argument. The following example is legal until C++20:
classContainer{public:int&operator[](inta){/*...*/}};...Containerc;c[1,4]=5;// <--
This mistake might make someone believe it’s about accessing a specific cell in a matrix located in row 1 column 4, when it actually ignores the1
and passes only the number4
to the operator function.
Since C++20 using the comma operator without parentheses (()
) inside subscript expressions isdeprecated, and since C++23 it’s illegal as the operator might accept more than one parameter and the compiler will generate a matching error to a wrong number of parameters.
In C++23 we can see an example of the usage of this new language ability in the implementation ofmdspan
(which was part of the motivation for this ability).
Usage example with parentheses:
Containerc;c[(1,4)]=5;// The intention is clear: Only one parameter is passed to the operator.
Overloading Comma Operator
Now that we understand the basic risk of using the comma operator, it’s time to have some fun with it and stretch the limits of this pranking operator.
template<typenameT>std::ostream&operator,(std::ostream&out,Tval){returnout<<val;}intmain(){std::cout,"hello ","world ",42;// Prints "hello world 42"return0;}
Yes, it’s legal. But we can stretch the rules a little bit further:
template<typenameT,typenameP>Toperator,(Tlhs,Prhs){returnlhs;}intmain(){intx=5,y=6,z;z=(x,y);std::cout<<z;// Prints 6std::stringstr1="5",str2="6",str3;str3=(str1,str2);std::cout<<str3;// Prints 5return0;}
This time it’s a little bit more confusing: Why does the new overload have no effect on the first case, and does have an effect on the second one?
The reason is that the comma operator overload (and any other operator’s overloading) has to accept at least one parameter of class or enumeration type. Therefore the instantiation ofint operator,(int, int)
is not legal and doesn’t apply to the first case (which uses the regular rule of comma operator).
If you also insist on forcing the first example to work by the new rule, we can make a little change:
structMyInt{MyInt(inta):a(a){}operatorint(){returna;}inta;};/*...*/MyIntx=5,y=6;intz;z=(x,y);std::cout<<z;// Prints 5
And a little bit of strings math:
usingnamespacestd::string_literals;intoperator,(std::string_views1,std::string_views2){std::cout<<s1<<" "<<s2;returns1[0]+s2[0];}intmain(){std::cout<<("Hello"s,"World\n")+("What's"s,"Up\n")*("Nicely"s,"Done\n")<<"\n";"This"s,"is"," so"s,"cool";return0;}/* Output: */HelloWorldWhat'sUpNicelyDone25271Thisissocool
Approaching large numbers without this operator
In math, we used to separate large numbers using commas. As we can’t use this approach in C++, we have to use other methods to help us with readability and maintainability issues.
Since C++14 we can use a single quote to split a number into multiple sections (cppreference):
unsignedlonglongl1=18446744073709550592ull;// C++11unsignedlonglongl2=18'446'744'073'709'550'592llu;// C++14unsignedlonglongl3=1844'6744'0737'0955'0592uLL;// C++14unsignedlonglongl4=184467'440737'0'95505'92LLU;// C++14
Another strategy is to use the literale
(orE
) followed by a number. This means you multiply the number beforee
by 10 raised to the power of the number aftere
. For example:
inta=10e3;// Equals to: 10,000floatb=15e-3;// Equals to: 0.015intc=15.3e6;// Equals to: 15,300,000
Conclusion
The comma operator is useful for separating commands inside a limited section. However, it’s a highly dangerous operator with non-obvious actions and meanings sometimes. If you see a usage of this comma operator, don’t ignore it and suspect it might cause an unseen problem.
The best practice for this particular operator is to avoid it as much as you can and prefer alternatives as much as possible. This operator can cause real damage, as shown in this article, even with subtle changes in the code.
Special thanks toYehezkel Bernat &Ellie Bogdanov for reviewing & comments.
This article originally published on my personal blog:C++ Senioreas.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse