Tom Moers
Tom Moers

Reputation: 1261

Move semantics & argument evaluation order

Considering the following:

std::string make_what_string( const std::string &id );

struct basic_foo
{
    basic_foo( std::string message, std::string id );
};

struct foo
    : public basic_foo
{
    foo::foo( std::string id)
        : basic_foo( make_what_string( id ), std::move( id ) ) // Is this valid?
    {
    }
};

Because parameter evaluation order in C++ is unspecified, I'm wondering if the line

basic_foo( make_what_string( id ), std::move( id ) )

in above code is valid.

I know that std::move is nothing more than a cast, but when is the std::string move ctor executed? After all arguments have been evaluated and it's time to call the base constructor? Or is this done during evaluation of the parameters? In other words:

Does the compiler do this:

std::string &&tmp2 = std::move(id);
std::string tmp1 = make_what_string(id);
basic_foo(tmp1, tmp2);

which is valid. Or this:

std::string tmp2 = std::move(id);
std::string tmp1 = make_what_string(id);
basic_foo(tmp1, tmp2);

which is invalid. Note that in both cases the order is the "unexpected" one.

Upvotes: 31

Views: 1131

Answers (2)

Joseph Mansfield
Joseph Mansfield

Reputation: 110658

See section 1.9:

Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced.

and

When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function. [ Note: Value computations and side effects associated with different argument expressions are unsequenced. —end note ]

I think the problem is that it's not very clear whether the initialization of the parameters is considered a side effect associated with the argument expressions. However, it appears to be backed up by section 5.2.2:

The initialization and destruction of each parameter occurs within the context of the calling function.

And there's also a note in the same paragraph that makes it a little clearer:

When a function is called, each parameter (8.3.5) shall be initialized (8.5, 12.8, 12.1) with its corresponding argument. [ Note: Such initializations are indeterminately sequenced with respect to each other (1.9) — end note ]

So yes, the initialization of the arguments are indeterminately sequenced with respect to each other. The initializations might occur in either of these orders:

std::string message = make_what_string(id);
std::string id = std::move( id );

std::string id = std::move( id );
std::string message = make_what_string(id);

In the second case, make_what_string ends up working with a moved-from string.

So, even though std::move doesn't actually move anything, the important thing is that the actual moving is also unsequenced with respect to the other argument.

The definition of the move constructor of basic_string(basic_string&& str) states:

[...] str is left in a valid state with an unspecified value.

So you don't have undefined behaviour, you have unspecified behaviour.

Upvotes: 20

It's not really valid. The order of function argument evaluation is unspecified. In other words, you don't know whether the compiler will choose this sequence:

tmp1 = make_what_string(id);
tmp2 = std::move(id);
basic_foo(tmp1, tmp2);

or this one:

tmp1 = std::move(id);
tmp2 = make_what_string(id);  //id has already been moved from!
basic_foo(tmp2, tmp1);

Upvotes: 7

Related Questions