移动语义和功能顺序评估

时间:2014-07-17 22:41:10

标签: c++ c++11 initialization move-semantics unspecified-behavior

假设我有以下内容:

#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)) {}
};

如果我理解关于&#34;未指定函数参数顺序的C ++规则&#34;没错,这段代码不安全。如果首先使用移动构造函数构造B构造函数的第二个参数,则a现在包含nullptr,而表达式a->x将触发未定义的行为(可能是段错)。如果首先构造第一个参数,那么一切都将按预期工作。

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

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

但是在课程初始化列表中,我们无法自由创建临时变量。

假设我无法更改B,是否有任何可能的方法来完成上述操作?即在相同的函数调用表达式中取消引用和移动unique_ptr而不创建临时的?

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

谢谢

3 个答案:

答案 0 :(得分:46)

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

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

来自§8.5.4/ 4 [dcl.init.list]

  

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

答案 1 :(得分:30)

作为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
    {}
};

答案 2 :(得分:9)

Praetorian建议使用列表初始化似乎有效,但它有一些问题:

  1. 如果unique_ptr参数首先出现,我们就不幸了
  2. B的客户容易忘记使用{}代替()B界面的设计者将这个潜在的错误强加给我们。
  3. 如果我们可以改变B,那么构造函数的一个更好的解决方案是总是通过rvalue引用而不是value来传递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};