Comma Operator RAII Abuse

by Malte Skarupke

Here’s a neat little trick that one of my co-workers, Clint Levijoki, discovered. In C++ you often use an RAII wrapper that you place on the stack if you want to be sure that code gets run at a later point. One good example would be std::lock_guard which you use if you want to be sure that a lock gets released in the future, or scoped profiling which you use to stop a timer in the future. For example for this:

std::string foo();
void bar()
{
    std::string baz = foo();
    // do something with baz
}

If you want to profile foo() you’d write it like this:

std::string foo();
void bar()
{
    std::string baz;
    {
        ScopedProfiler profile_foo("foo()");
        baz = foo();
    }
    // do something with baz
}

Which is less pretty and slightly slower. Alternatively you can use the comma operator and do it like this:

std::string foo();
void bar()
{
    std::string baz = (ScopedProfiler("foo()"), foo());
    // do something with baz
}

And this will start a timer before calling foo(), and stop the timer after calling foo(). You could wrap it in a macro to make it more readable. And the benefit is obviously that you don’t have to destroy your function flow when you want to insert RAII objects.

There are two parts of the standard that are interacting here. Part 5.18.1, which is about the comma operator, states this:

A pair of expressions separated by a comma is evaluated left-to-right; the left expression is a discarded-value expression […]. Every value computation and side effect associated with the left expression is sequenced before every value computation and side effect associated with the right expression.

But then part 12.2.3 which is about temporary objects states this:

Temporary objects are destroyed as the last step in evaluating the full-expression […] that (lexically) contains the point where they were created. This is true even if that evaluation ends in throwing an exception. The value computations and side effects of destroying a temporary object are associated only with the full-expression, not with any specific subexpression.

So side effects associated with destructors follow different rules, and thus don’t follow the side effect rule of the comma operator. And that makes the above hack possible.

Unfortunately you still want to be careful with this. Let’s say that you wrap the above in a macro so that it reads like this:

std::string foo();
#define PROFILE(func) (ScopedProfiler(#func), func)
void bar()
{
    std::string baz = PROFILE(foo());
    // do something with baz
}

Then you’re likely to run into a problem if you try to use the macro inside of a function call, like this:

std::string foo();
void take_string(int, std::string);
#define PROFILE(func) (ScopedProfiler(#func), func)
void bar()
{
    take_string(5, PROFILE(foo()));
}

In this case the destructor of the temporary would be called after take_string() has finished, not after foo() has finished as you would expect from reading the code. I do not know a good way of fixing this.

I haven’t used this trick yet because it is easy to use it in a way that causes problems or which at least makes your code less readable. But there are objects for which the example without the comma operator would be slower than the alternative with the comma operator, and the comma operator trick may be useful then.