call operator argument evaluation order

Viewed 95
#include <string>

struct X
{
    char y;
    std::string z;

    X & operator()(std::string && s)
    {
        z = std::move(s);
        return *this;
    }

    X & operator()(char c)
    {
        y = c;
        return *this;
    }
};

int main()
{
    X x;
    std::string y("abc");

    x(y[0])(std::move(y));
}

is the last line of main undefined behavior? I'm guessing yes because it would unfold to something like the following but just want to make sure that there are no stricter guarantees on call operators or member function invocations in general

X::operator()(&X::operator()(&x, y[0]), std::move(z))

Please add references from the standard or cppref

1 Answers

Before c++17, chaining function calls that modify the same l-value, like in your example, is indeed undefined behavior, since the order of evaluation of these expressions is unspecified.

However, a proposal to fix that was merged into c++17.

Here's the relevant rule (emphasis mine), which also contains an example from the proposal that shows how this works:

The postfix-expression is sequenced before each expression in the expression-list and any default argument. The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter. [Note: All side effects of argument evaluations are sequenced before the function is entered (see [intro.execution]). — end note] [Example:

void f() {
 std::string s = "but I have heard it works even if you don't believe in it";
 s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don't"), 6, "");
 assert(s == "I have heard it works only if you believe in it");       // OK
}

— end example]


While the above rule only strictly refers to the built-in operator(), and you have user-defined operators, the same rules about order of evaluation apply, because of this rule:

If either operand has a type that is a class or an enumeration, a user-defined operator function might be declared that implements this operator or a user-defined conversion can be necessary to convert the operand to a type that is appropriate for a built-in operator. ... However, the operands are sequenced in the order prescribed for the built-in operator.

Related