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

ead*_*ead 8 c++ gcc rvo c++11

标准不需要编译器执行返回值优化(RVO),但是从C ++ 11开始,必须移动结果。

看来,这可能会将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;
}
Run Code Online (Sandbox Code Playgroud)

当使用gcc(或与此相关的clang)进行编译时(为了简化示例,-O2 -fno-inline -fno-elide-constructors我正在使用std::vector这些build-option。如果没有这些选项以及手工类和更复杂的create功能,则可能会触发相同的行为)对于C ++ 98(-std=c++98)一切正常:

  1. return a;触发复制构造函数,它保持a原样。
  2. 的Destructor m称为(必须在a被销毁之前发生,因为它m是在之后构造的a)。a在析构函数中访问是没有问题的。
  3. 的析构a函数称为。

结果与预期的一样:21已打印(此处为live)。

但是,使用C ++ 11(-std=c++11)构建时,情况则有所不同:

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

这是一个现场演示

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

Nat*_*ica 4

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

int main(){
    Vec v;
    Manager m(v);
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • @ead是的,你写的代码没有UB。我的示例中的代码确实如此,即使是在 C++98 中也是如此。我指出,您的析构函数对于该语言的任何版本都是不正确的,因为您对您的向量做出了一个您无法知道是否正确的假设。由于您采用任何向量,其中包括空向量,这意味着您的析构函数不正确,因为它无条件访问它。 (4认同)