DEV Community

Coral Kashri
Coral Kashri

Posted on • Originally published at cppsenioreas.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:


int a, b;

a = 5, b = 4, b += a, ++a, std::cout << b << " " << a; // Prints 9 6

Enter fullscreen mode Exit fullscreen mode

Another example of that operator usage is as follows:


for (size_t i = 0, k = 500; i < 10; ++i, ++k) { /*...*/ }

Enter fullscreen mode Exit fullscreen mode

We can see this operator in action in the third section of the for 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 <typename T, typename V> void func(int, int, int);
int a = 5, b = 6, c = 7; // Not the comma operator
for (int i = 1, j = 2; i < 5; ++i); // Not the comma operator
func<int, float>(a, b, c); // Not the comma operator

Enter fullscreen mode Exit fullscreen mode

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 of fold 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>
void call_functions(ArgsF&&... argsf) {
    (argsf(), ...);
}
/*...*/
call_functions(
  [] { std::cout << "Func1\n"; },
  [] { std::cout << "Func2\n"; },
  [] { std::cout << "Func3\n"; }
);

Enter fullscreen mode Exit fullscreen mode

The ‘,’ is out there

About a year ago someone published on Reddit the following example:


for (int i = 0; i < 10,000; i += 1)

Enter fullscreen mode Exit fullscreen mode

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 and 000. 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:


int x = 5, y = 6, z;
z = (x, y);

Enter fullscreen mode Exit fullscreen mode

In this example, the two expressions are x and y. The compiler first evaluates x and then y and returns the latter. But what would happen if we remove the parentheses?


int x = 5, y = 6, z;
z = x, y;

Enter fullscreen mode Exit fullscreen mode

Well, this time the expressions are z = x and y. 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:


class Container {
public:
    int& operator[](int a) { /*...*/ }
};

...

Container c;
c[1, 4] = 5; // <--

Enter fullscreen mode Exit fullscreen mode

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 the 1 and passes only the number 4 to the operator function.

Since C++20 using the comma operator without parentheses (()) inside subscript expressions is deprecated, 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 of mdspan (which was part of the motivation for this ability).

Usage example with parentheses:


Container c;
c[(1, 4)] = 5; // The intention is clear: Only one parameter is passed to the operator.

Enter fullscreen mode Exit fullscreen mode

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 <typename T>
std::ostream& operator,(std::ostream& out, T val)
{
    return out << val;
}

int main() {
    std::cout, "hello ", "world ", 42; // Prints "hello world 42"
    return 0;
}

Enter fullscreen mode Exit fullscreen mode

Yes, it’s legal. But we can stretch the rules a little bit further:


template <typename T, typename P>
T operator,(T lhs, P rhs)
{
    return lhs;
}

int main() {
    int x = 5, y = 6, z;
    z = (x, y);
    std::cout << z; // Prints 6

    std::string str1 = "5", str2 = "6", str3;
    str3 = (str1, str2);
    std::cout << str3; // Prints 5
    return 0;
}

Enter fullscreen mode Exit fullscreen mode

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 of int 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:


struct MyInt {
    MyInt(int a) : a(a) {}
    operator int() { return a; }
    int a;
};
/*...*/
MyInt x = 5, y = 6;
int z;
z = (x, y);
std::cout << z; // Prints 5

Enter fullscreen mode Exit fullscreen mode

And a little bit of strings math:


using namespace std::string_literals;

int operator,(std::string_view s1, std::string_view s2)
{
    std::cout << s1 << " " << s2;
    return s1[0] + s2[0];
}

int main() {
    std::cout << ("Hello"s, "World\n") + ("What's"s, "Up\n") * ("Nicely"s, "Done\n") << "\n";
    "This"s, "is", " so"s, "cool";
    return 0;
}

/* Output: */
Hello World
What's Up
Nicely Done
25271
This is so cool

Enter fullscreen mode Exit fullscreen mode

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):


unsigned long long l1 = 18446744073709550592ull; // C++11
unsigned long long l2 = 18'446'744'073'709'550'592llu; // C++14
unsigned long long l3 = 1844'6744'0737'0955'0592uLL; // C++14
unsigned long long l4 = 184467'440737'0'95505'92LLU; // C++14

Enter fullscreen mode Exit fullscreen mode

Another strategy is to use the literal e (or E) followed by a number. This means you multiply the number before e by 10 raised to the power of the number after e. For example:


int a = 10e3; // Equals to: 10,000
float b = 15e-3; // Equals to: 0.015
int c = 15.3e6; // Equals to: 15,300,000

Enter fullscreen mode Exit fullscreen mode

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 to Yehezkel Bernat & Ellie Bogdanov for reviewing & comments.

This article originally published on my personal blog: C++ Senioreas.

Top comments (0)