混合 boost::optional 和 std::unique_ptr

Mix boost::optional and std::unique_ptr

我承认:我爱上了可选的概念。自从我发现它以来,我的代码质量有了很大的提高。明确变量是否有效比简单的错误代码和带内信号要好得多。它还让我不必担心必须阅读文档中的合同,或担心它是否是最新的:代码本身就是合同。

也就是说,有时我需要处理 std::unique_ptr。这种类型的对象可能为空,也可能不是;在代码中的给定点不可能知道 std::unique_ptr 是否应该有值;不可能从代码中知道合约。

我想以某种方式混合optional(可能与boost::optional)和std::unique_ptr,这样我就有了一个动态分配的对象,它具有范围破坏和正确的 copy/move 行为,明确声明它可能没有值 。这样,我可以使用这种新类型来明确表示必须检查值,并避免对普通 std::unique_ptr.

进行不必要的检查

在 C++11 标准中是否有用于此的工具,boost 或足够流行的库?我可以接受为此定义我自己的 class,但这是最不受欢迎的方法(由于缺乏彻底的测试)。

因此,为了概括您的问题,您需要:

  1. 由value/on堆栈分配的非可选类型:您很高兴直接为此使用对象类型。
  2. 由 value/on 堆栈分配的可选类型:您很高兴为此使用 boost::optional(或者您可以使用 C++17 中的 std::optional)。
  3. 在堆上分配并拥有指向对象的非可选类型。
  4. 在堆上分配并拥有指向对象的可选类型。

你很不高兴你可以表达1和2的区别,但是3和4通常使用相同的类型(std::unique_ptr)。您建议对 3 使用 std::unique_ptr,绝不允许 nullptr,对 4 使用其他一些东西,但想知道您可以使用什么。 (在评论中,如果可以为 3 找到其他东西,您也可以接受将 std::unique_ptrnullptr 用于 4 的可能性。)

您问题的字面答案:您可以简单地使用 boost::optional<std::unique_ptr<T>> 表示 4(而按照您的建议使用裸 unique_ptr 表示 3)。

您问题的替代文字答案: 正如@StoryTeller 所说,您可以定义自己的智能指针类型,类似于 unique_ptr 但不允许 nullptr ,并将其用于 3。更快(但非常肮脏)的替代方法是强制函数对 unique_ptr 和对同一对象的引用的 return a pair。然后只通过引用访问结果,但只有在 unique_ptr 仍然存在时才这样做:

template<class T>
using RefAndPtr = std::pair<T&, std::unique_ptr<T>>;

RefAndPtr<Foo> getFoo()
{
    std::unique_ptr<Foo> result = std::make_unique<Foo>();
    return RefAndPtr<Foo>(*result, std::move(result));
}

我的实际建议: 接受并为 3 和 4 使用 std::unique_ptr。在类型系统中阐明你的意图是一件好事,但也许多好事可能是坏事。使用上述任一选项只会让阅读您的代码的任何人感到困惑。即使您阻止人们错误地传递 nullptr,如何阻止他们传递指向错误对象或已释放内存等的指针?在某些时候,您必须指定类型系统之外的内容。

std::unique_ptr 可以为空。每当移出或默认构造时,它都会变为 null。

std::unique_ptr 是您的可空堆分配对象。

一个value_ptr可以写成不可为空的。注意搬家有额外费用:

template<class T>
class value_ptr {
  struct ctor_key_token{ explicit ctor_key_token(int){} };
public:
  template<class A0, class...Args, class dA0 = std::decay_t<A0>,
    std::enable_if_t<!std::is_same<dA0, ctor_key_token>{} && !std::is_same<dA0, value_ptr>{}, int> = 0
  >
  value_ptr( A0&& a0, Args&&... args):
    value_ptr( ctor_key_token(0), std::forward<A0>(a0), std::forward<Args>(args)... )
  {}
  value_ptr(): value_ptr( ctor_key_token(0) ) {}

  template<class X, class...Args>
  value_ptr( std::initializer_list<X> il, Args&&... args ):
    value_ptr( ctor_key_token(0), il, std::forward<Args>(args)... )
  {}

  value_ptr( value_ptr const& o ):
    value_ptr( ctor_key_token(0), *o.state )
  {}
  value_ptr( value_ptr&& o ):
    value_ptr( ctor_key_token(0), std::move(*o.state) )
  {}

  value_ptr& operator=(value_ptr const& o) {
    *state = *o.state;
    return *this;
  }
  value_ptr& operator=(value_ptr && o) {
    *state = std::move(*o.state);
    return *this;
  }

  T* get() const { return state.get(); }
  T* operator->() const { return get(); }
  T& operator*() const { return *state; }

  template<class U,
    std::enable_if_t<std::is_convertible<T const&, U>{}, int> =0
  >
  operator value_ptr<U>() const& {
    return {*state};
  }
  template<class U,
    std::enable_if_t<std::is_convertible<T&&, U>{}, int> =0
  >
  operator value_ptr<U>() && {
    return {std::move(*state)};
  }
private:
  template<class...Args>
  value_ptr( ctor_key_token, Args&&... args):
    state( std::make_unique<T>(std::forward<Args>(args)...) )
  {}

  std::unique_ptr<T> state;
};

这是不可为空的堆分配值语义对象的粗略草图。

请注意,当您从它移动时,它不会释放旧内存。它唯一不拥有堆上的 T 的时间是在构造期间(只能通过抛出中止)和销毁期间(因为 state 被销毁)。

更高级的版本可以有自定义的破坏者、克隆者和移动者,允许存储多态值语义类型或不可复制的类型。

使用几乎从不为空或很少为空的类型作为从不为空会导致错误。所以不要这样做。

Live example.

在 C++ 的类型系统中,编写不可为 null 的 unique_ptr 是不可能的。 unique_ptr 可为空 而不是 只是惯例。这是许多评论中严重遗漏的要点。移动构造函数会是什么样子?这一点之前已经讲过:https://youtu.be/zgOF4NrQllo?t=38m45s。由于不可为空的 unique_ptr 类型是不可能的,因此在任何一种情况下您都可以使用 unique_ptr 指针。

如果需要,您可以创建一个类似于 unique_ptr 的指针类型,但没有 public 默认构造函数。每次移动时它仍会进入空状态。这并没有给你太多保证,但它给了你一点,它可以作为文档。我认为这种类型的价值不足以证明它的存在。

我建议简单地在代码库中制定一个约定,即 std::unique_ptr 总是指向某些东西 除非 它是在构造函数中初始化的 class 成员或者刚刚被取消引用并且即将超出范围(并且只有在这些情况下它可能包含 null)