关于c ++:移动语义和函数顺序评估

Move semantics and function order evaluation

假设我有以下内容:

1
2
3
4
5
6
7
8
9
10
#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将触发未定义的行为(可能是段错误)。 如果首先构造第一个参数,那么一切都会按预期进行。

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

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

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

假设我不能更改B,有什么可能的方法来完成上述任务? 即在同一函数调用表达式中取消引用并移动unique_ptr而不创建临时文件?

如果可以更改B的构造函数但不添加诸如setX(int)之类的新方法怎么办? 有帮助吗?

谢谢


使用列表初始化来构造B。 这样就保证了从左到右对元素进行评估。

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

从§8.5.4/ 4 [dcl.init.list]

Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions (14.5.3), are evaluated in the order in which they appear. That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list.


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

1
2
3
4
5
6
7
8
9
10
11
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
    {}
};


Praetorian关于使用列表初始化的建议似乎可行,但存在一些问题:

  • 如果参数unique_ptr首先出现,那我们就不走运了
  • 对于B的客户来说,这种方法太容易了,以至于意外忘记使用{}而不是()B界面的设计者已将此潜在错误强加给我们。
  • 如果我们可以更改B,那么对于构造函数来说,更好的解决方案是始终通过rvalue引用而不是按值传递unique_ptr。

    1
    2
    3
    4
    5
    struct A { int x; };

    class B {
      B(std::unique_ptr<A>&& a, int x) : _x(x), _a(std::move(a)) {}
    };

    现在我们可以安全地使用std :: move()了。

    1
    2
    B b(std::move(a), a->x);
    B b{std::move(a), a->x};