52

假设我有以下内容:

#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现在包含 anullptr并且表达式a->x将触发未定义的行为(可能是 segfault)。如果首先构造第一个参数,那么一切都会按预期工作。

如果这是一个普通的函数调用,我们可以创建一个临时的:

auto x = a->x
B b{x, std::move(a)};

但是在类初始化列表中我们没有创建临时变量的自由。

假设我不能改变B,有没有可能的方法来完成上述?即unique_ptr在同一个函数调用表达式中取消引用和移动 a 而不创建临时?

如果您可以更改B的构造函数但不能添加新的方法,例如setX(int)怎么办?那会有帮助吗?

谢谢

4

3 回答 3

47

使用列表初始化构造B. 然后保证从左到右评估元素。

C(std::unique_ptr<A> a) : B{a->x, std::move(a)} {}
//                         ^                  ^ - braces

§8.5.4/4 [dcl.init.list]

花括号初始化列表的初始化列表中初始化子句包括任何由包扩展 (14.5.3) 产生的子句,按照它们出现的顺序进行评估。也就是说,与给定初始化子句相关联的每个值计算和副作用在初始化器列表的逗号分隔列表中与任何初始化子句相关联的每个值计算和副作用之前进行排序。

于 2014-07-17T22:47:24.630 回答
33

作为 Praetorian 答案的替代方案,您可以使用构造函数委托:

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
    {}
};
于 2014-07-17T23:12:06.363 回答
11

Praetorian 的使用列表初始化的建议似乎可行,但它有几个问题:

  1. 如果 unique_ptr 参数先出现,我们就不走运了
  2. 它太容易让客户B不小心忘记使用{}而不是(). 的界面设计者将B这个潜在的错误强加给了我们。

如果我们可以更改 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};
于 2014-07-17T23:07:31.813 回答