The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal

Comma operator in C++

Notes published the
7 - 8 minutes to read, 1693 words
Categories: c c++
Keywords: c c++ comma operator operator overloading

This is the sister article to macro usages in C++, where to use it, and how to avoid it.

Contrary to macros, the comma operators are not used as much.

For most use cases it is an obsolete construct, for others, there is still no replacement, and in some cases, it is the most readable alternative.

gotcha with the comma operator

overloaded comma operator

One of the main reasons one should want to avoid using the comma operator is because it can be overloaded. Personally, I never had a use-case where I wanted to execute an overloaded comma operator, and the general guideline is in fact: "do not overload the comma operator".

When writing i, j, in most cases, we want this to be equivalent to i;j.

Note 📝
The notes are mainly for C++, but some considerations also hold for C.

If we need to use the comma operator, we can leverage the fact that it is not possible to overload the comma operator between a type and void. Thus there are two common patterns to avoid calling the overloaded version: Either add a void() between the statements, like i, void(), j, or cast one expression to void, like i, void(j).

There are surely use cases where overloading the comma operator might provide a nice interface or is generally useful; that has not been my experience so far.

Also, note that defining the comma operator wrongly can have unexpected side effects.

Access to uninitialized data

Comma operators (and lambdas) make it "easy" to access uninitialized data accidentally.

struct data{
	data(int);
};
data foo();
void bar(const data& d);

void bar(){
    data d = (bar(d), void(), foo());
}

In this example bar has access to data d before foo is executed.

A similar error can also happen with a lambda

struct data{
	// no default constructor
	data(int);
};
data foo();
void bar(const data& d);

void bar(){
    data d = [&](){bar(d); return foo();}();
}

As of today, no compiler (GCC, clang, and MSVC) warns about those constructs.

In this example, avoiding to use of a lambda and the comma operator does make it harder to access an uninitialized variable by accident.

While it is true that one could write int i = i + 5;, I never saw it happen by accident, the code is short enough that such an error is (hopefully) immediately fixed.

On the contrary, with the comma operator, and especially a lambda, there is more code, and thus it is easier to overlook a mistake:

struct data{
	// no default constructor
	data(int);
};
data foo();
void bar(const data& d);

void bar(){
	const data d = [&](){
		auto d_ = foo();
		bar(d);  // error, should be bar(d_);
		return d_;
	}();
}

comma operator inside loop

The comma operator is often used in a loop when someone needs to update multiple counters

void bar(int j){
	for(int i=0; i!=10; ++i, ++j){
		std::printf("%d, %d\n", i, j);
	}
}

In this case, the comma operator can be replaced with an immediately invoked lambda, which is much less readable, thus generally not recommended:

void bar(int j){
	for(int i=0; i!=10;[&]{++i;++j;}()){
		std::printf("%d, %d\n", i, j);
	}
}

In this example, between int, there cannot be a overloaded comma operator, but there could be if the types were iterators!

Casting one expression to void() helps, but the simpler alternative would be to change one of the variables inside the body of the loop

void bar(int j){
	for(int i=0; i!=10; ++i){
		++j;
		std::printf("%d, %d\n", i, j);
	}
}

Macros

Comma operators are also used in macros, in particular in multi-statement macros! Workarounds and alternatives have already been documented.

C++11 constexpr function

When C++11 got standardized, constexpr functions were much more limited.

They could consist of only one return statement.

Thus people needed to get creative for creating more complex constexpr functions, in particular, the ternary operator (?:) and the comma operator were the main tools for defining the body of such functions.

Given such constraints, it is probably not possible to avoid using the comma operator.

For temporary variables

The comma operator was also useful for reducing the scope of temporary variables.

Consider the following example

struct timer{
	timer(); // starts measuring time
	~timer(); // prints how much time passed since construction
};

std::string expensive_function();



void bar(){
	// ...
	std::string result;
	{
		auto t = timer();
		result = expensive_function();
	}

	// ...
}

Thanks to timer, it is easy to measure how much time expensive_function takes, but one needs to pay attention to the scope of the variables.

To reduce the scope at a minimum, one needs to add a { and } that will probably take up two further lines.

And there is always a chance that something else ends in that scope too.

One easy way to reduce the scope even further is to use the comma operator:

struct timer{
	timer(); // starts measuring time
	~timer(); // prints how much time passed since construction
};

std::string expensive_function(int);


void bar(){
	// ...
	std::string result = (timer(), void() expensive_function(42));
	// ...
}

Another advantage of this approach is that it can avoid a two-phase-initialization.

For code reusability, one might want to wrap such a pattern in a macro

struct timer{
	timer(); // starts measuring time
	~timer(); // prints how much time passed since construction
};
#define MEASURE(...) (timer(), void(), __VA_ARGS__)
std::string expensive_function(int);


void bar(){
	// ...
	std::string result = MEASURE(expensive_function(42));
	// ...
}

But wrapping it in a macro makes it too easy to use it incorrectly:

struct timer{
	timer(); // starts measuring time
	~timer(); // prints how much time passed since construction
};
#define MEASURE(...) (timer(), void(), __VA_ARGS__)
std::string expensive_function(int);

void foo(std::string_view);
void bar(){
	// ...
	foo(MEASURE(expensive_function(42)));
	// ...
}

In this example, ~timer() is executed after foo finishes, and thus the timer is not measuring only expensive_function anymore.

In this case, an immediately invoked lambda helps to fix the issue (check the table)

struct timer{
	timer(); // starts measuring time
	~timer(); // prints how much time passed since construction
};
#define MEASURE(...) [&]{ auto t = timer(); __VA_ARGS__ ;}()
std::string expensive_function(int);

void foo(std::string_view);
void bar(){
	// ...
	foo(MEASURE(expensive_function(42)));
	// ...
}

If you are thinking about how to avoid the macro, the easiest way is inverting the control flow, and having a a function that takes a callback, although the source code will be more verbose:

struct timer{
	timer(); // starts measuring time
	~timer(); // prints how much time passed since construction
};
decltype(auto) measure(F f){
	auto t = timer();
	return f();
}

std::string expensive_function(int);
void foo(std::string_view);

void bar(){
	// ...
	foo(measure([]{return expensive_function(42);}));
	// ...
}

For detecting member variables and functions

As documented, the comma operator, together with decltype can be used for detecting member variables.

if and switch statements

bool variables and variables of types that are convertible to bool can be initialized in an if statement easily

bool foo();
void bar(){
	// ...
	if(bool b = foo()){
		// ...
	}
	// ...
}

If a type is "only" explicitly convertible to bool through a function or member function, then the code will not compiler

struct data{
	bool empty();
};
data foo();
void bar(){
	// ...
	if(data d = foo()){ // will not compile
		// ...
	}
	// ...
}

A common workaround seems to be to use the comma operator, forget that it can be overloaded, and leverage two-phase initialization

struct data{
	bool empty();
};
data foo();
void bar(){
	// ...
	data d;
	if(d = foo(), void(), d.empty()){
		// ...
	}
	// ...
}

I’m not sure why someone would come up with this solution, but I’ve seen it in the wild too many times. Personally, at this point, why not initialize the data directly?

struct data{
	bool empty();
};
data foo();
void bar(){
	// ...
	data d = foo();
	if(d.empty()){
		// ...
	}
	// ...
}

Both approaches have the disadvantage that data d is observable outside of the if-statement, which can be changed by adding an additional scope

struct data{
	bool empty();
};
data foo();
void bar(){
	// ...
	{
		data d = foo();
		if(!d.empty()){
			// ...
		}
	}
	// ...
}

but it is not really practical.

A lambda could help here too, but only if discarding the data after the test is fine, thus normally not applicable (and also not that readable)

struct data{
	bool empty();
};
data foo();
void bar(){
	// ...
	if([]{data d = foo(); return !d.empty();}()){
		// ...
	}
	// ...
}

Note that the code with the lambda is error-prone.

In a first draft I wrote if([]{data d = foo(); return !d.empty();}) instead of if([]{data d = foo(); return !d.empty();}()). This first snippet is equivalent to if( []{data d = foo(); return !d.empty();} != nullptr), and the condition is always true.

I should not write it, but you might be better served with a macro

#define INIT_IF(...) [&] __VA_ARGS__ ()

struct data{
	bool empty();
};
data foo();
void bar(){
	// ...
	if(INIT_IF({data d = foo(); return !d.empty();})){
		// ...
	}
	// ...
}

Another approach would be to define a wrapper structure that is implicitly convertible to bool

struct data{
	bool empty();
};

struct data_bool{
	data value;
	explicit operator bool(){
		return !d.empty();
	}
}

data foo();
void bar(){
	// ...
	if(auto d_ = data_bool{foo()}){
		auto& d_ = d.value;
		// ...
	}
	// ...
}

Although writing such a wrapper is simple, it is in most cases not worth it.

The best approach is available since C++17 with the if statement with initializer:

struct data{
	bool empty();
};

data foo();
void bar(){
	// ...
	if(data d = foo(); !d.empty()){
		// ...
	}
	// ...
}
Note 📝
While all examples had an if-statement, the same holds for switch.

Variadic functions

There are a couple of use cases with variadic functions, where there is no alternative to the comma operator.

One example would be calling a given function for every variadic parameter:

void foo(int);

// calls a given function for every listed parameter
template <class F, typename... P>
void call(F f, P&&... params){
	((f(params), void()),...);
}

int main(){
	call(foo, 1, 0, -1, 42);
	// equivalent to
	//foo(1); foo(0); foo(-1); foo(42);
}

Other examples can be found at foonathan::blog()

Conclusion

There are some cases where you need to use the comma operator, or where the comma operator is the most readable alternative.

In most cases, there is little to no reason to use it.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.