C++ Comma Operator series:

In this article I will explain some nice real world usages for the comma operator.

“There appear to be few practical uses of operator,()”
Bjarne Stroustrup, The Design and Evolution of C++

Oh, Bjarne, you are totally right.



Conclusions first


Comma operator is tricky and nowadays (C++17) using it won’t help you much (except for some cases, explained in this article).

Overloading the comma operator with explicit types is a bit tricky too, as you can easily end up with hidden fallbacks to the language behavior if the specific overload isn’t matched. The worst part is that there is no possible error/warning when this happens (example of this nasty bug). This problem can be solved using proxy templates (explained later in C++03 Params example) and explicitly casting all operands to void to ensure calling the non-overloaded operator.

Questionable practices, read carefully

I’m not responsible for any brain or code damages



Beautiful loop


Imagine the situation: we need to perform some action in a loop before checking the loop condition, for every cycle.

processWindowEvents(window);

while (window.isOpen())
{
    // render, etc
    
    processWindowEvents(window);
}

having duplicated logic outside of the block looks… strange…..

Look at the comma version of the same algorithm:

while (processWindowEvents(window), window.isOpen())
{
    // render, etc
}

I personally prefer the comma version, I think it’s more readable and less susceptible to algorithmic bugs (after refactors, etc) than the other.



Maintaining multiple variables in limited places


No, I’m not talking about declaring multiple variables, I mean maintaining them.

Let’s imagine that we are creating a very simple “Big O calculator” by having an extra variable that is incremented on every loop. We can use comma to perform the increment without adding extra statements into our loop body:

size_t n = 100, bigO = 0;

for (size_t i = 0; i < n; ++i, ++bigO) // maintain 2 variables
{
    // very interesting task
}

for (size_t i = 0; i < n/2; ++i, ++bigO) // thanks comma <3
{
    // another interesting task
}

cout << "O(" << bigO/(double)n << "n)"; // O(1.5n) = O(n+n/2)

Declaring multiple variables in a single line by using the comma
int x = 0, y = 33, z = 666, ... is something totally different than using comma operator, but they both use the same syntax/grammar.



SFINAE: decltype magic with custom type deduction


If you ever tried to master this demon, I’m pretty sure you have seen things like decltype(T::something(), 0) that you don’t really understand but you just copy-paste them because, well… they work.

But, what is that evil comma in decltype doing? Let’s imagine this simple type trait:

// SFINAE fallback
template<typename T, typename = int>
struct HasHelloFunction : std::false_type {};

// true case specialization
template<typename T>
struct HasHelloFunction<T, decltype(T::hello(), 0)> : std::true_type {};

If we remove the , 0 part it stops working well. Since decltype resolves the type of the internal expression, we can use the comma operator to force the final resolution to a specific type (int) if the compilation succeeds.

bool hello();

decltype(hello()); // bool
decltype(0); // int
decltype(hello(), 0); // (bool, int) => int

In our example, we want to specialize our template for HasHelloFunction<T, int> if T::hello function exists.

We don’t care about the return type of T::hello(). In case our expression compiles (function exists and can be invoked with zero args), we want to deduce our SFINAE fallback type: int. As simple as that.

This is the perfect example when using the comma in decltype helps us avoiding long and complex template activation traits.

(full example source code)

T::hello() might return a type that overloads the comma operator. This affects our expected behavior as it could be that (T::hello(), 0) no longer returns int(0). To be safe from our side, we must explicitly cast the method call result to void. The proper implementation is decltype((void)T::hello(), 0)

As long as every previous sub-expression compiles, compiler will continue evaluating them. The behavior is very similar to std::enable_if_t but with a different/custom resolution type.

(C++14) Using decltype(0, auto) for the return type doesn’t deduct the proper type due to more complex technical limitations. If you want to learn more about this edge case please add a comment below.

(C++17) void_t<...> was added to avoid the need of manual resolution to fallback type. Proper C++17 implementation can be found here.



(C++03) Params of specific type


In modern C++, passing unlimited arguments of a specific type is very easy with variadic templates, std::initializer_list or brace initializers. For old C++03, explicit overloads or variadic args are the only options… (or not? …)

Explicit overloads are nice and simple if you have a few defined usages and you know how many args you will need, but isn’t scalable at all.

Variadic args is something that comes from C world: it only works in runtime (not nice, as you get no compile-time errors) and does implicit conversions instead of throwing errors for incorrect types. Definitely a really bad option.

va_arg(args, T) will internally apply a C style cast to the arg to type T, allowing weird implicit conversions.

(full example of the variadic args problem)

My comma-based solution:

Why don’t we create a class that overloads the comma operator for pushing parameters into a std::vector<T>?

template<typename T>
struct Params : public std::vector<T>
{
    template<typename V> // avoids fallback to language-defined behavior
    inline Params<T>& operator,(const V& value)
    {
        this->push_back(value);
        return *this;
    }
};

This technique is accepted and used in tons of nice libraries. For example boost::spirit uses the operator,, boost::assign uses operator+= and boost::format uses operator% for similar purposes.

We can use Params<T> in our functions like this:

void printParams(const Params<int>& params = Params<int>())
{
    cout << "[";

    for (size_t i = 0; i < params.size(); ++i)
    {
        cout << (i == 0 ? "" : ", ");
        cout << params[i];
    }

    cout << "]" << endl;
}

and pass the parameters using the comma operator

// No params
printParams(); // []

// No params with explicit empty params
printParams(Params<int>()); // []

// Some params
printParams((Params<int>(), 1, 2, 3)); // [1, 2, 3]

// Compile-time error: invalid conversion from `const char[6]` to `int`
printParams((Params<int>(), 1, 2, 3, "error"));

This method is the best for C++03 as it provides nice syntax and compile-time errors.

(full working example)

If your compiler supports C++11 or greater

Use std::initializer_list<T> + brace initializer for this case. It was introduced to avoid those nasty tricks. No possible discussion.



(C++11) Bypass constexpr restrictions


In C++11, constexpr function body is limited to a simple return expression. This forced us to use smart ways to combine multiple expressions, being comma one of the most used ones as it ensures a perfectly defined evaluation order. Typical example is compile time assertions + return:

template<size_t Index, size_t N>
constexpr bool checkArrayBounds()
{
    return Index < N || (throw "Index out of bounds", false);
}

template<size_t Index, typename T, size_t N>
constexpr T array_at(T(&array)[N]) // `array` is a reference to `T[N]`
{
    return checkArrayBounds<Index, N>(), *(array+Index); // Comma magic *-*
}

(full working example)



(C++17) Fold expressions (reducing parameter packs)


One of the best features of C++17, fold expression, allows reducing a parameter pack by applying a binary operator (including comma). This opens a huge branch of possibilities like calling a function for each argument or easily getting the last argument from the pack.

Let’s take advantage of the comma operator to perform an action for each argument, for example, pushing every argument from the pack to a std::vector:

template<typename T, typename... Args>
void push_back_vec(std::vector<T>& v, Args&&... args)
{
    ((void)v.push_back(std::forward<Args>(args)), ...);
}

Bugs in cppreference

This example is taken from cppreference, but their version doesn’t implement perfect forwarding properly, std::forward is needed here! Also note the defense against any possible comma operator overloads by casting the result to void.

(full working example)



Obfuscation


Take a “good practices guide” and reverse it. You will get the definitive guide for creating unmaintainable code (bible of code obfuscation). One of the main rules will be: use the comma operator frequently (YESSS!).

Here’s a simple mind-blowing example that takes advantage of commas, enjoy :D

char* c{ (char*)-1 };
if (c = 0, delete c++, c--)
{
    long a[] = { 0b1100, 0x1, 0b10, 0b110, 0x10 }, b = (long)c;
    char(*p)(char) = [](char x) { return printf("%c", 0x43 + x), x; };
    for (int i = (c++, p(b)); ++c, i < 5 || p(b); p(b | a[i]), ++i, c++);
}

(full working example)



The end


That’s all! I didn’t want to end up with a huge post so I omitted many other even more questionable uses of the comma operator, listed below.

Now that you are a real expert on the comma operator, what do you think about it? Drop a comment below!

Edit: Thanks a lot for your comments! I did some changes based on them :)



If you want to get informed about my new articles, check the Subscribe and Atom feed buttons in the top menu.

Thanks for reading!