在通过将 unique_ptr 移动到同一对象而构造的基 class 之后,使用指向对象的原始指针初始化字段

Initialize field with raw pointer to object after base class constructed with moved unique_ptr to same object

我有一个对象 Foo 属于 Owner,没有其他人拥有。我创建了一个继承 Owner 的 class Derived 对象,将其传递给新创建的 Foo unique_ptrDerived 有一个 class User 的成员也需要使用 Foo,但不拥有它,所以它只得到一个指向它的原始指针。

#include <memory>
#include <iostream>
using namespace std;

struct Foo {
  void talk() { cout << "foo" << endl; }
};

struct User {
  Foo *foo;
  User(Foo *_foo): foo(_foo) {};
  void use() { foo->talk(); } //potential disaster if foo's memory has changed
};

struct Owner {
  unique_ptr<Foo> foo;
  Owner(unique_ptr<Foo> _foo): foo(move(_foo)) {};
};

struct Derived : public Owner {
  User user;
  Derived(unique_ptr<Foo> _foo)
  : Owner {move(_foo)},
    user{_foo.get()}  //potential disaster: _foo has already been move()'d, can't get()!
  {};
  void use() { user.use(); };
};

int main() {
  Derived d(make_unique<Foo>());
  //do other stuff
  d.use();  //potential disaster
}

问题是在 Derived 的初始化列表中,我需要 move()Foo 传递给 Owner 的构造函数(需要一个unique_ptr<Foo>,因为它是所有者),并将指向 Foo 的原始指针传递给 User。但是当 User 被初始化时,unique_ptr<Foo> 已经被移动并且 foo.get() 可能指向一个无效的位置!

确保 User 获得指向 FooOwner 仍获得其 unique_ptr<Foo> 的有效(非拥有)指针的好方法是什么?

您可以在复制指针时将工作委托给私有构造函数:

struct Derived : public Owner {
  User user;
  Derived(unique_ptr<Foo> _foo)
  : Derived(move(_foo), _foo.get())
  {};

  void use() { user.use(); };
private:
  // Implement the real logic here
  // As pointed by @Artyer in the comments, this r-value ref is required. 
  // Pass by value would again introduce UB in the calling code.
  Derived(unique_ptr<Foo>&& _foo, Foo* ptr)
   : Owner {move(_foo)},
    user{ptr} {}
};

请注意,即使未指定求值顺序,Derived(move(_foo),_foo.get()) 也是安全的,因为 std::move 只是一个转换。

为了清楚起见,这不是一场潜在的灾难,而是一场特定的灾难 因为初始化列表是从左到右求值的。

9.4.4.4 [dcl.init.list]:

Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions ([temp.variadic]), 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. [Note: This evaluation ordering holds regardless of the semantics of the initialization; for example, it applies when the elements of the initializer-list are interpreted as arguments of a constructor call, even though ordinarily there are no sequencing constraints on the arguments of a call. — end note] .

添加:为什么私有构造函数需要通过引用传递

@Artyer 用 counter-example 发布了正确的解释,但不幸的是从那时起就删除了它。

有两种合理的方式来编写委托构造函数:

Derived(unique_ptr<Foo>&& _foo, Foo* ptr) //(A)
Derived(unique_ptr<Foo> _foo, Foo* ptr) //(B) Since the move is cheap.

假设它是这样调用的:

Derived(move(_foo), _foo.get()) //(1)
Derived{move(_foo), _foo.get()} //(2) @Artyer's idea

首先,通过添加委托构造函数,我们实质上添加了一个排序点,该排序点必须在将唯一指针和原始指针分别传递给 OwnerUser 之前对其进行评估。

其次,函数调用的所有参数必须在实际调用函数之前计算,此顺序对于 () 和 left-to-right 未指定 {}(请参阅删除上一段)。

区别就在这里,如果使用(B),则必须在调用位置进行复制,而对于(),可以在将_foo.get()分配给[=21]之前进行此复制=] 参数。对于 {} 它总是会发生。因此设置 ptr 不正确。

另一方面,如果选择 (A),则只有 r-value 引用被复制到参数中,并且 ptr 将始终获得正确的值,无论是否 (1)或使用 (2)

您可以使您的基础成员 class 受到保护(或更高的可访问性)并引用基础成员

Derived(unique_ptr<Foo> _foo)
  : Owner {move(_foo)},
    user{foo.get()} // refers to Owner::foo