在C ++中移动语义-局部变量的移动返回


11

我的理解是,在C ++ 11中,当您按值从函数返回局部变量时,允许编译器将该变量视为r值引用,并将其“移出”函数以返回它(如果当然不会发生RVO / NRVO)。

我的问题是,这不能破坏现有代码吗?

考虑以下代码:

#include <iostream>
#include <string>

struct bar
{
  bar(const std::string& str) : _str(str) {}
  bar(const bar&) = delete;
  bar(bar&& other) : _str(std::move(other._str)) {other._str = "Stolen";}
  void print() {std::cout << _str << std::endl;}

  std::string _str;
};

struct foo
{
  foo(bar& b) : _b(b) {}
  ~foo() {_b.print();}

  bar& _b;
};

bar foobar()
{
  bar b("Hello, World!");
  foo f(b);

  return std::move(b);
}

int main()
{
  foobar();
  return EXIT_SUCCESS;
}

我的想法是,本地对象的析构函数可能会引用隐式移动的对象,从而意外地看到“空”对象。我试图测试这个(见http://ideone.com/ZURoeT),但我得到了“正确”的结果没有明确std::movefoobar()。我猜这是由于NRVO引起的,但是我没有尝试重新排列代码来禁用它。

我是正确的,因为这种转换(导致移出函数)是隐式发生的,并且可能破坏现有代码?

更新 这里是一个示例,它说明了我在说什么。以下两个链接用于相同的代码。 http://ideone.com/4GFIRu-C ++ 03 http://ideone.com/FcL2Xj-C ++ 11

如果查看输出,则有所不同。

因此,我想这个问题现在变成了,在向标准中添加隐式移动时是否考虑了这个问题,并且决定可以添加此重大更改,因为这种代码非常稀有吗?我也想知道在这种情况下是否有任何编译器会发出警告...


移动必须始终使对象处于可破坏状态。
Zan Lynx 2014年

是的,但这不是问题。在c ++ 11之前的代码中,可以假设局部变量的值不会仅仅因为返回而改变,因此这种隐式移动可能会破坏该假设。
Bwmat 2014年

这就是我在示例中试图阐明的内容;通过析构函数,您可以在return语句执行之后但函数实际返回之前检查函数局部变量(子集)的状态。
Bwmat 2014年

对于您添加的示例,这是一个很好的问题。我希望这可以从可以阐明这一点的专业人士那里获得更多答案。我能给出的唯一真实反馈是:这就是为什么对象通常不应该具有非所有权数据视图的原因。当您为对象提供非拥有视图(原始指针或引用)时,实际上有很多方法可以编写无缺陷的代码,从而产生段错误。如果您愿意,我可以在适当的答案中进行详细说明,但我想这并不是您真正想听到的。顺便说一句,已经知道11可以破坏现有代码,仅通过定义新关键字即可。
尼尔·弗里德曼

是的,我知道C ++ 11从未声称不会破坏任何旧代码,但这相当微妙,而且很容易错过(没有编译器错误,警告和segfaults)
Bwmat

Answers:


8

Scott Meyers 发表于comp.lang.c ++(2010年8月),涉及一个隐式生成move构造函数可能会破坏C ++ 03类不变式的问题:

struct X
{
  // invariant: v.size() == 5
  X() : v(5) {}

  ~X() { std::cout << v[0] << std::endl; }

private:    
  std::vector<int> v;
};

int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

这里的问题是,在C ++ 03中,Xv成员始终具有5个元素。X::~X()依靠该不变式,但是新引入的move构造函数从移出v,从而将其长度设置为零。

这与您的示例有关,因为仅在X的析构函数中检测到了破碎的不变式(正如您所说,本地对象的析构函数有可能引用隐式移动的对象,因此意外地看到一个对象)。

C ++ 11试图在打破一些现有代码与基于move构造函数提供有用的优化之间取得平衡。

委员会最初决定,当用户不提供移动构造函数和移动赋值运算符时,它们应由编译器生成。

然后确定这确实是引起警报的原因,并且它以某种方式限制了移动构造函数和移动赋值运算符的自动生成,以致打破现有代码(例如,明确定义的析构函数)的可能性很小,尽管并非不可能。

诱人的是,认为当存在用户定义的析构函数时阻止生成隐式move构造函数就足够了,但事实并非如此(N3153-隐式Move必须进入更多细节)。

N3174中-移动或不移动 Stroupstrup表示:

我认为这是语言设计问题,而不是简单的向后兼容性问题。避免破坏旧代码很容易(例如,仅从C ++ 0x中删除移动操作),但是我看到通过使移动操作无处不在成为使C ++ 0x成为更好的语言,为此值得破坏一些C + +98码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.