在 RAII 构造中修改 RVO 值是否安全?

Is it safe to modify RVO values within an RAII construct?

考虑以下程序:

#include <functional>
#include <iostream>

class RvoObj {
  public:
    RvoObj(int x) : x_{x} {}
    RvoObj(const RvoObj& obj) : x_{obj.x_} { std::cout << "copied\n"; }
    RvoObj(RvoObj&& obj) : x_{obj.x_} { std::cout << "moved\n"; }

    int x() const { return x_; }
    void set_x(int x) { x_ = x; }

  private:
    int x_;
};

class Finally {
  public:
    Finally(std::function<void()> f) : f_{f} {}
    ~Finally() { f_(); }

  private:
    std::function<void()> f_;
};

RvoObj BuildRvoObj() {
    RvoObj obj{3};
    Finally run{[&obj]() { obj.set_x(5); }};
    return obj;
}

int main() {
    auto obj = BuildRvoObj();
    std::cout << obj.x() << '\n';
    return 0;
}

clang 和 gcc (demo) 输出 5 而不调用复制或移动构造函数。

此行为是否由 C++17 标准明确定义和保证?

复制省略仅允许实现删除函数生成的对象的存在。也就是说,它可以删除从objfoo的return值对象的副本和obj的析构函数。但是,实施不能改变任何其他内容。

复制到 return 值将发生在调用函数中局部对象的析构函数之前。并且 obj 的析构函数将在 run 的析构函数之后发生,因为自动变量的析构函数以其构造的相反顺序执行。

这意味着 run 在其析构函数中访问 obj 是安全的。 obj 表示的对象是否在 run 完成后销毁都不会改变这个事实。

但是,有一个问题。请参阅,调用 move 操作需要局部变量 return <variable_name>;。在您的情况下,从 RvoObj 移动与从中复制相同。所以对于你的特定代码,它会很好。

但是如果 RvoObj 是,例如 unique_ptr<T>,你就有麻烦了。为什么?因为对 return 值的移动操作发生在 调用局部变量的析构函数之前 。所以在这种情况下 obj 将处于移动状态,对于 unique_ptr 意味着它是空的。

太糟糕了。

如果这一步被省略,那就没问题了。但是由于不需要省略,因此可能存在问题,因为您的代码会根据是否发生省略而表现不同。这是实现定义的。

所以一般来说,最好不要让析构函数依赖于您正在 returning 的局部变量的存在。


以上内容纯粹与您关于 未定义行为 的问题有关。根据是否发生省略来做一些改变行为的事情不是 UB。该标准定义了一种或另一种会发生。

但是,您不能也不应该依赖它

简短回答:由于 NRVO,程序的输出可能是 35。两者都有效。


背景先看:

  • What are copy elision and return value optimization?

指南:

  • 避免修改 return 值的析构函数。

例如,当我们看到下面的模式时:

T f() {
    T ret;
    A a(ret);   // or similar
    return ret;
}

我们需要问自己:A::~A() 是否以某种方式修改了我们的 return 值?如果是,那么我们的程序很可能有错误。

例如:

  • 销毁时打印 return 值的类型很好。
  • 一种在销毁时计算 return 值的类型

[来自 ]