考虑以下程序:
#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标准明确定义并保证?
简短回答:由于NRVO,程序的输出可能是3
或5
。两者都有效。
有关背景,请首先参阅:
指南:
例如,当我们看到以下模式时:
T f() {
T ret;
A a(ret); // or similar
return ret;
}
我们需要自问:A::~A()
是否会以某种方式修改我们的返回值?如果是,则我们的程序很可能存在错误。
例如:
obj
到foo
的返回值对象和obj
的析构函数的副本。但是,该实现无法更改其他任何内容。复制到返回值将在调用函数中的本地对象的析构函数之前发生。并且obj
的析构函数将在run
的析构函数之后发生,因为自动变量的析构函数以其构造的相反顺序执行。
这意味着run
在其析构函数中访问obj
是安全的。在obj
完成之后是否破坏由run
表示的对象不会改变这一事实。
[然而,有一个问题。请参见return <variable_name>;
,以获取调用move操作的局部变量。在您的情况下,从RvoObj
中移动与从中复制相同。因此,对于您的特定代码,就可以了。
RvoObj
例如是unique_ptr<T>
,您会遇到麻烦。为什么?因为对返回值的移动操作发生在调用局部变量的析构函数之前
。因此,在这种情况下,obj
将处于移出状态,对于unique_ptr
而言,意味着它为空。不好。如果放弃此举,那就没有问题。但是由于不需要省略,因此存在潜在的问题,因为代码会根据是否发生省略而表现不同。这是实现定义的。
因此,一般而言,最好不要让析构函数依赖于要返回的局部变量的存在。
未定义行为
的问题有关。根据是否发生省略来更改行为不是UB。该标准定义一个或另一个将发生。但是,您不能也不应该依靠它
。