移动返回对象的构造函数会破坏C ++ 98代码?

时间:2019-04-24 11:04:05

标签: c++ c++11 gcc rvo

该标准不需要编译器执行返回值优化(RVO),但是自C ++ 11起,结果为must be moved

似乎,这可能会将UB引入到/破坏代码中,这在C ++ 98中是有效的。

例如:

#include <vector>
#include <iostream>

typedef std::vector<int> Vec;
struct Manager{
    Vec& vec;
    Manager(Vec& vec_): vec(vec_){}
    ~Manager(){
        //vec[0]=42; for UB
        vec.at(0)=42;
    }
};

Vec create(){
    Vec a(1,21);
    Manager m(a);
    return a;
}

int main(){
    std::cout<<create().at(0)<<std::endl;
}

当使用-O2 -fno-inline -fno-elide-constructors用gcc(或用clang编译)时(为了简化示例,我在std::vector中使用了这些build-option。这些带有手工类和更复杂的create函数的选项)对于C ++ 98(-std=c++98)来说都是可以的:

  1. return a;触发复制构造函数,这使a保持完整。
  2. 调用m的析构函数(必须在a被解构之前发生,因为m是在a之后构造的)。在析构函数中访问a是没有问题的。
  3. 调用a的析构函数。

结果符合预期:打印21here live)。

然而,当构建为C ++ 11(-std=c++11)时情况就不同了:

  1. return a;触发移动构造器,该构造器“销毁” a
  2. 调用了m的析构函数,但是现在访问a是有问题的,因为a已移动并且不再完整。
  3. vec.at(0)现在抛出。

这里是live-demonstration

我是否缺少某些东西,并且示例在C ++ 98中也有问题?

3 个答案:

答案 0 :(得分:4)

这不是重大变化。您的代码已经在C ++ 98中注定了。想象你有

int main(){
    Vec v;
    Manager m(v);
}

在上面的示例中,当m被销毁时,您访问了向量,并且由于向量为空,因此引发了异常(如果使用[],则具有UB)。这与从vec返回create时遇到的相同情况至关重要。

这意味着您的析构函数不应该对其类成员的状态进行假设,因为它不知道它们处于什么状态。要使您的析构函数对任何版本的C ++都是“安全的”,您需要将在try-catch块中调用at,否则您需要测试向量的大小以确保它等于或大于期望的大小。

答案 1 :(得分:2)

“我只是不敢相信有效代码会变得无效...” 是的,它确实可能会变得无效。另一个例子:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
int stoi(const basic_string<T>& str)
{ 
  return 0;
}

int main()
{
  std::string s("-1");
  int i = stoi(s);
  std::cout << s[i];
}

该代码在C ++ 98/03中有效,但在C ++ 11中具有UB。


关键是这样的代码或您的代码是极端情况,在实践中通常不会出现。它们(几乎)始终代表着非常糟糕的编码实践。如果遵循良好的编码习惯,那么从C ++ 98迁移到C ++ 11时,您可能不会遇到任何麻烦。

答案 2 :(得分:1)

您的代码根据应用RVO(不使用<?php进行编译)还是创建临时文件以返回结果(使用-fno-elide-constructors)来公开各种行为。

使用RVO时,C ++ 98和C ++ 11的结果相同,为42。但是引入临时变量将在C ++ 98中隐藏对42的最终赋值,该函数将返回结果21。在C ++ 11版本中,随着使用-fno-elide-constructors语义创建临时文件时,情况变得更糟,因此,将对象分配给移动的(如此空的)对象将导致异常。

外卖课程只是为了避免将具有副作用的任何代码也放到析构函数和构造函数中。