【问题标题】:Move semantics & argument evaluation order移动语义和参数评估顺序
【发布时间】:2013-03-18 19:28:22
【问题描述】:

考虑以下几点:

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?
    {
    }
};

由于未指定 C++ 中的参数评估顺序,我想知道是否 这条线

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

上面的代码是有效的。

我知道std::move 只不过是一个演员表,但什么时候是 std::string 移动 ctor 被执行?在评估了所有参数之后,是时候调用了 基础构造函数?或者这是在评估参数期间完成的?在 换句话说:

编译器会这样做吗:

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

这是有效的。或者这样:

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

这是无效的。请注意,在这两种情况下,订单都是“意外的” 一个。

【问题讨论】:

  • 其实代码是有效的。但是,我相信您的意思是在base_foo 的构造函数中通过右值引用而不是值来获取字符串id(不是吗?)。
  • @CassioNeri,代码不符合预期:)
  • 是的,我在午餐时间意识到了这一点。 :-) 非常好的问题。

标签: c++ c++11 move-semantics operator-precedence


【解决方案1】:

参见第 1.9 节:

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的。

调用函数时(无论函数是否内联),与任何参数表达式或指定被调用函数的后缀表达式相关的每个值计算和副作用,都在执行每个表达式或语句之前排序被调用函数的主体。 [ 注意:与不同参数表达式相关的值计算和副作用是无序的。 —尾注 ]

我认为问题在于,参数的初始化是否被认为是与参数表达式相关的副作用并不是很清楚。但是,它似乎得到了第 5.2.2 节的支持:

每个参数的初始化和销毁​​都发生在 调用函数。

在同一段中还有一个注释,使它更清楚一点:

当调用函数时,每个参数 (8.3.5) 都应使用其对应的参数初始化 (8.5, 12.8, 12.1)。 [注意:这样的初始化相对于彼此的顺序是不确定的(1.9)-结束说明]

所以是的,参数的初始化相对于彼此是不确定的。初始化可能按以下任一顺序进行:

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

在第二种情况下,make_what_string 最终处理的是移出的字符串。

因此,即使std::move 实际上并没有移动任何东西,重要的是实际移动 相对于另一个参数也是无序的。

basic_string(basic_string&& str) 的移动构造函数的定义声明:

[...] str 处于未指定值的有效状态。

所以你没有未定义的行为,你有未指定的行为。

【讨论】:

  • @David:是的,因为从字符串移动可能不再有效,具体取决于实现
  • @Tom 实际上,它是未指定的,而不是未定义的。 :)
  • 我认为规范参数可以是:参数在函数调用之前或之后初始化。函数调用后是不可能的。因此它必须发生在之前。并且参数初始化通常被定义为在调用者的上下文中发生(因此它不会在“被调用函数的主体”中进行评估)。因此,对于参数评估,参数初始化没有排序(也没有传递排序)。
  • @sftrabbit 是的,注释是错误的,因为“不确定排序”意味着初始化不能相互重叠。在某些情况下,对于类类型的情况甚至不是这样,因为在这些情况下,有时可能会为单个初始化完成两个函数调用(一个在初始化表达式的类对象的转换函数上,一个在副本/移动参数类型的类对象的构造函数,如果没有省略。在这之间,另一个初始化可能会干扰)。
  • 还要考虑这个:int x = 0; void f(int a, int b) { cout << a << b; } struct A { operator int&() { x++; return x; } }; int main() { f(A(), A()); }f 可能会输出 1 22 1(如果初始化是“不确定的顺序”,则需要)。但它也可能会输出2 2(初始化是交错的,可能是它们未排序的结果)。 ICC为我输出“2 2”,GCC“2 1”和Clang“1 2”(都在-O2)。
【解决方案2】:

这不是真的有效。未指定函数参数评估的顺序。也就是说,你不知道编译器会不会选择这个序列:

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

或者这个:

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

【讨论】:

  • 你确定吗?我更新了我的问题,以便更好地说明我在问什么。
  • 但是std::move 本身实际上并没有移动任何东西,它只是转换为一个右值引用。
  • @interjay:确实;但是(如问题所述)从std::move 的结果中形成函数参数的值 确实 移动它。
  • @MikeSeymour:我知道,但是这个答案中显示的“无效”代码实际上是有效的,假设 tmp1 是一个右值引用。正如 sftrabbit 的回答所说,问题在于参数初始化的顺序,而不是参数评估的顺序。
  • 问题是参数求值是否包括参数的初始化。
猜你喜欢
  • 1970-01-01
  • 2018-11-05
  • 2011-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-28
相关资源
最近更新 更多