假设我有以下内容:
#include <memory>
struct A { int x; };
class B {
B(int x, std::unique_ptr<A> a);
};
class C : public B {
C(std::unique_ptr<A> a) : B(a->x, std::move(a)) {}
};
如果我正确理解了c++关于"函数参数的未指定顺序"的规则,那么这段代码是不安全的。如果B
的构造函数的第二个参数首先使用move构造函数构造,那么a
现在包含nullptr
,并且表达式a->x
将触发未定义的行为(可能是段错误)。如果先构造第一个实参,那么一切都将按预期工作。
如果这是一个普通的函数调用,我们可以创建一个临时的:
auto x = a->x
B b{x, std::move(a)};
但是在类初始化列表中,我们没有创建临时变量的自由。
假设我不能改变B
,是否有任何可能的方法来完成上述?即解引用和移动unique_ptr
在同一函数调用表达式没有创建一个临时?
如果你可以改变B
的构造函数,但不添加新的方法,如setX(int)
?这有帮助吗?
谢谢
使用列表初始化来构造B
。然后保证元素从左到右求值。
C(std::unique_ptr<A> a) : B{a->x, std::move(a)} {}
// ^ ^ - braces
源自§8.5.4/4 [dcl.init.list]
在带括号的初始化列表的初始化列表中,初始化子句,包括任何由包展开(14.5.3)产生的子句,按它们出现的顺序求值。也就是说,与给定初始化子句相关的每个值计算和副作用都在初始化子句后面的初始化子句相关的每个值计算和副作用之前排序。
除了Praetorian的答案,你还可以使用constructor delegate:
class C : public B {
public:
C(std::unique_ptr<A> a) :
C(a->x, std::move(a)) // this move doesn't nullify a.
{}
private:
C(int x, std::unique_ptr<A>&& a) :
B(x, std::move(a)) // this one does, but we already have copied x
{}
};
禁卫军的建议使用列表初始化似乎工作,但它有几个问题:
- 如果unique_ptr参数先出现,我们就不走运了
B
的客户端很容易忘记使用{}
而不是()
。B
接口的设计者强加给我们这个潜在的bug。如果我们可以改变B,那么构造函数的一个更好的解决方案可能是始终通过右值引用而不是通过值传递unique_ptr。
struct A { int x; };
class B {
B(std::unique_ptr<A>&& a, int x) : _x(x), _a(std::move(a)) {}
};
现在可以安全地使用std::move()了
B b(std::move(a), a->x);
B b{std::move(a), a->x};
代码不包含未定义行为。这是一个常见的误解,std::move()实际上执行了一个移动,它没有。Std::move()只是将输入强制转换为r值引用,这是一个语义编译时更改,没有运行时代码。因此在语句中:
B(a->x, std::move(a))
'a'的状态不会被std::move()调用修改,因此无论求值顺序如何,都不会有未定义的行为。