在 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 标准明确定义和保证?
复制省略仅允许实现删除函数生成的对象的存在。也就是说,它可以删除从obj
到foo
的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,程序的输出可能是 3
或 5
。两者都有效。
背景先看:
- 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 值的类型不。
[来自 ]
考虑以下程序:
#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 标准明确定义和保证?
复制省略仅允许实现删除函数生成的对象的存在。也就是说,它可以删除从obj
到foo
的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,程序的输出可能是 3
或 5
。两者都有效。
背景先看:
- 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 值的类型不。
[来自