混合使用boost::optional和std::unique_ptr

8
我承认:我热爱可选项的概念。自从我发现它以来,我的代码质量得到了很大提高。明确变量是否有效比纯错误代码和带内信号更好。这还使我不必担心必须阅读文档中的合同,或者担心它是否最新:代码本身就是合约。
话虽如此,有时我需要处理std::unique_ptr。这种类型的对象可能为null或非null;在代码的某个给定点上,无法知道std::unique_ptr是否应该有一个值;从代码中无法知道合同。
我想以某种方式混合optional(也许与boost::optional一起)和std::unique_ptr,以便我有一个具有动态分配对象、作用域销毁和适当复制/移动行为的新类型,它明确声明可能没有值。这样,我可以使用这种新类型来明确表示需要检查值并避免对纯std::unique_ptr进行不必要的检查。
在C++11标准、boost或足够流行的库中有此类工具吗?我可以接受为此定义自己的类,但这将是最不受欢迎的方法(由于缺乏彻底的测试)。

4
表达清晰是好的,但也需要考虑惯例。指针类型从古至今都是可以为null值的。 - StoryTeller - Unslander Monica
1
@StoryTeller 发明了空指针,称其为“价值十亿美元的错误”,也许现在是该修复它的时候了。 - n. m.
1
unique_ptr 是可空的,你不能将其视为非空。 - n. m.
1
我认为正确的方法不是使用optional。依靠约定来表示空值,返回unique_ptr。并定义一个新的智能指针类型(可能通过继承自unique_ptr实现),强制执行非空不变量。这样,当您在代码库中看到新类型时,您就知道无需检查,而当您看到旧类型时,约定会警告用户。 - StoryTeller - Unslander Monica
1
如果你的惯例适用于你,还需要什么呢?可空指针?这里:template <typename T> using nullable_unique_ptr = unique_ptr<T>; 这种类型是按照惯例可空的。 - n. m.
显示剩余15条评论
4个回答

6
所以,总结一下你的问题,你想要:
  1. 一个按值分配/在堆栈上分配的不可选类型:你可以直接使用对象类型。
  2. 一个按值分配/在堆栈上分配的可选类型:你可以使用boost::optional(或者你可以使用C++17中的std::optional)。
  3. 一个在堆上分配并拥有指向对象的非可选类型。
  4. 一个在堆上分配并拥有指向对象的可选类型。

你不满意的是你可以表达1和2之间的区别,但通常3和4都使用相同的类型(std::unique_ptr)。你建议对3使用std::unique_ptr,从不允许nullptr,并对4使用其他东西,但想知道可以使用什么(在评论中,如果可以为3找到其他内容,也接受使用带nullptrstd::unique_ptr)。

字面回答你的问题:你可以简单地使用boost::optional<std::unique_ptr<T>>来表示4(同时像你建议的那样对3使用裸unique_ptr)。

字面上的替代回答:正如@StoryTeller所说,你可以定义自己的智能指针类型,就像unique_ptr一样,但不允许nullptr,并在3中使用它。一个更快(但非常肮脏)的替代方法是强制函数返回同时包含unique_ptr和对同一对象的引用的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));
}

我的建议:就用std::unique_ptr来解决3和4两个问题吧。在类型系统中澄清意图是一件好事,但过分做也会产生不良影响。使用上述选项之一只会让任何阅读您代码的人混乱不堪。即使你能阻止别人错误地传递nullptr,怎么防止他们传递指向错误对象或已释放内存等问题呢?在某些情况下,你必须在类型系统之外指定一些东西。


你的总结是正确的(尽管我要补充说明,在第三和第四种情况中,对象需要有明确的所有权规则,所以不要使用裸指针)。我曾经考虑过boost::optional<std::unique_ptr<T>>,但认为这会让人感到困惑(另外在这个新类中,将内部unique_ptr抽象化处理并返回对内部对象的引用,将会很棒),而且我也不确定boost::optional是否能处理unique_ptr的微妙问题。 - user2891462
1
好的,我更新了3和4来提到所有权。我还添加了一个快速的hack来处理非空的unique_ptr-like对象,但是这个方法非常不规范! - Arthur Tacca
2
你也可以使用 gsl::not_null<std::unique_ptr<T>> 作为一种约定。 - Caleth
@Caleth:我认为gsl::not_null不支持std::unique_ptr... - Jarod42
@Caleth 值得添加为单独的答案。 - Arthur Tacca

2

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被销毁)。
更高级的版本可以有自定义销毁器、克隆器和移动器,允许存储多态值语义类型或不可复制类型。
将几乎永远不为空或很少为空的类型用作永不为空会导致错误。所以不要这样做。
示例:在线示例

当然,struct S { int m[1000]; }; using Sptr = value_ptr<S>; 总是很糟糕的。 - aschepler
@aschepler 如果您确保所有移动都是省略,或者可能添加一个交换专业化,您就可以避免它成为垃圾。现在我想到了,我们也可以通过交换来改进=(&&) - Yakk - Adam Nevraumont

0

在C++的类型系统中,不可能编写一个非空的unique_ptrunique_ptr可为空并不仅仅是一种约定。这是许多评论中被忽略的重点。移动构造函数会是什么样子?这一点以前已经讨论过:https://youtu.be/zgOF4NrQllo?t=38m45s。由于不可能有非空的unique_ptr类型,因此您可以在任何情况下使用unique_ptr指针。

如果您想要,可以创建一个指针类型,就像unique_ptr一样,但没有公共默认构造函数。每次从中移动时,它仍将进入空状态。这并不能为您提供太多保证,但它可以作为文档。我认为这种类型不值得存在。


可能存在一种类似于shared_ptr但没有nullptr的东西。虽然这样做是可行的,但会增加额外的运行时开销。 - Arthur Tacca

0

我建议在代码库中简单地约定,std::unique_ptr 总是指向某个对象,除非它是在构造函数内初始化的类成员或者刚被解引用并即将超出作用域(只有在这些情况下才可能为空)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接