`std :: optional`优于`std :: shared_ptr`和`std :: unique_ptr`的优势是什么?

时间:2017-03-21 14:22:52

标签: c++ optional c++17 stdoptional

std::optional的推理是made by saying,它可能包含也可能不包含值。因此,如果我们不需要它,它可以节省构建一个可能是大对象的工作量。

For example,这里的工厂,如果不满足某些条件,将无法确定对象:

#include <string>
#include <iostream>
#include <optional>

std::optional<std::string> create(bool b) 
{
    if(b)
        return "Godzilla"; //string is constructed
    else
        return {}; //no construction of the string required
}

但是,这与此有何不同:

std::shared_ptr<std::string> create(bool b) 
{
    if(b)
        return std::make_shared<std::string>("Godzilla"); //string is constructed
    else
        return nullptr; //no construction of the string required
}

我们通过仅仅使用std::optional添加std::shared_ptr来获胜是什么?

5 个答案:

答案 0 :(得分:9)

  

通过在一般情况下仅使用std :: shared_ptr添加std :: optional,我们赢得了什么?

我们假设您需要从带有标记&#34的函数返回符号;而不是值&#34;。如果您使用std::shared_ptr,则会产生巨大的开销 - char将在动态内存中分配,而std::shared_ptr将保留控制块。虽然std::optional在另一边:

  

如果可选项包含值,则保证该值   作为可选对象足迹的一部分分配,即没有动态   内存分配一直在发生。因此,可选对象建模   对象,而不是指针,即使运算符*()和operator-&gt;()   已定义。

因此不涉及动态内存分配,即使与原始指针进行比较也可能很重要。

答案 1 :(得分:4)

指针可能为NULL,也可能不为NULL。这对你来说意味着什么完全取决于你。在某些情况下,nullptr是您处理的有效值,而在其他情况下,它可以用作标记以表示“无值,移动​​”。

对于std::optional,有一个明确的定义“包含值”和“不包含值”。您甚至可以使用带可选的指针类型!

这是一个人为的例子:

我有一个名为Person的类,我想懒洋洋地从磁盘加载他们的数据。我需要指出是否已加载某些数据。让我们使用指针:

class Person
{
   mutable std::unique_ptr<std::string> name;
   size_t uuid;
public:
   Person(size_t _uuid) : uuid(_uuid){}
   std::string GetName() const
   {
      if (!name)
         name = PersonLoader::LoadName(uuid); // magic PersonLoader class knows how to read this person's name from disk
      if (!name)
         return "";
      return *name;
   }
};

很好,我可以使用nullptr值来判断名称是否已从磁盘加载。

但是如果一个字段是可选的呢?也就是说,PersonLoader::LoadName()可能会为此人返回nullptr。我们真的是否希望每次有人请求此名称时

输入std::optional。现在我们可以跟踪我们是否已经尝试加载名称,如果该名称为空。如果没有std::optional,对此的解决方案是为名称创建布尔isLoaded,实际上每个可选字段。 (如果我们“将标志封装到一个结构中”会怎么样?嗯,那么你已经实现了optional,但做得更糟糕了:

class Person
{
   mutable std::optional<std::unique_ptr<std::string>> name;
   size_t uuid;
public:
   Person(size_t _uuid) : uuid(_uuid){}
   std::string GetName() const
   {
      if (!name){ // need to load name from disk
         name = PersonLoader::LoadName(uuid);
      }
      // else name's already been loaded, retrieve cached value
      if (!name.value())
         return "";
      return *name.value();
   }
};

现在我们不需要每次都去磁盘; std::optional允许我们检查。我在评论中写了一个small example,在较小的范围内展示了这个概念

答案 2 :(得分:3)

重要的是,如果您尝试从可选的value()访问optional时,您将获得已知的,可捕获的异常,而不是未定义的行为。因此,如果shared_ptr出现问题,那么调试时间比使用*之类的话要好得多。 (请注意,optional上的value()解除引用运算符在这种情况下仍会提供UB;使用value_or是更安全的选择。

此外,(t == nullptr) ? "default" : *t 等方法的一般方便性使您可以非常轻松地指定“默认”值。比较:

t.value_or("default")

optional

后者更具可读性,也更短。

最后,optional中项目的存储位于对象内。这意味着如果对象不存在,optional需要更多的存储指针;但是,这也意味着不需要动态分配将对象放入空System.IndexOutOfRangeException

答案 3 :(得分:1)

可选是可以为空的值类型。

shared_ptr是可以为空的引用计数引用类型。

unique_ptr是一个可移动的引用类型,可以为空。

他们的共同点是,他们可以自负 - 他们可以“缺席”#34;

它们不同,两个是引用类型,另一个是值类型。

值类型有一些优点。首先,它不需要在堆上进行分配 - 它可以与其他数据一起存储。这消除了可能的异常源(内存分配失败),可以快得多(堆比堆栈慢),并且更加缓存友好(因为堆往往相对随机排列)。

参考类型还有其他优点。移动引用类型不需要移动源数据。

对于非移动的引用类型,您可以使用不同的名称对同一数据进行多次引用。具有不同名称的两个不同值类型始终指的是不同的数据。无论哪种方式,这都是优点或缺点;但它确实使关于值类型的推理更容易。

关于shared_ptr的推理非常困难。除非对如何使用它进行非常严格的控制,否则几乎不可能知道数据的生命周期是什么。关于unique_ptr的推理要容易得多,因为您只需要跟踪其移动的位置。关于optional一生的推理是微不足道的(好吧,就像你嵌入它一样简单)。

可选接口增加了一些类似monadic的方法(如.value_or),但这些方法通常很容易添加到任何可空类型中。尽管如此,目前他们只有optional而不是shared_ptrunique_ptr

可选的另一个好处是,非常清楚您希望它有时可以为空。 C ++中存在一个习惯,即假设指针和智能指针不为空,因为它们的使用原因是其他而不是可为空。

因此代码假设某些共享或唯一的ptr永远不为null。它通常有用。

相比之下,如果你有一个可选项,你拥有它的唯一原因是因为它有可能实际上是空的。

在实践中,我谨慎地将unique_ptr<enum_flags> = nullptr作为参数,我想说&#34;这些标志是可选的&#34;,因为强制调用者的堆分配似乎无礼。但是optional<enum_flags>并没有强制调用者。 optional的非常便宜使我愿意在很多情况下使用它,如果我唯一可以自由的类型是智能指针,我会发现其他一些工作。

这消除了&#34;标志值&#34;的诱惑,如int rows=-1;optional<int> rows;具有更清晰的含义,并且在调试中会告诉我何时使用行而不检查&#34;空&#34;状态。

可以合理地失败或不返回任何感兴趣的函数可以避免标记值或堆分配,并返回optional<R>。例如,假设我有一个可放弃的线程池(比如,一个线程池在用户关闭应用程序时停止处理)。

我可以从&#34;队列任务返回std::future<R>&#34;函数和使用异常表示线程池已被放弃。但这意味着必须审核所有线程池的使用,以及来自&#34;异常代码流。

相反,我可以返回std::future<optional<R>>,并向用户提供他们必须处理的提示&#34;如果流程从未发生过会发生什么情况&#34;在他们的逻辑中。

&#34;来自&#34;异常仍然可以发生,但它们现在是例外,不是标准关闭程序的一部分。

在某些情况下,一旦符合标准,expected<T,E>将是更好的解决方案。

答案 4 :(得分:0)

  

通过在一般情况下仅使用std :: shared_ptr添加std :: optional,我们赢得了什么?

@Slava提到了不执行内存分配的优势,但这是一项附加优势(好吧,在某些情况下可能会带来很大好处,但我的观点是,它不是主要的优势)。

主要好处是(恕我直言)更清晰的语义

返回指针通常意味着(在现代C ++中)“分配内存”,或“处理内存”,或“知道内存中此地址和那个地址”。

返回一个可选值意味着“没有这个计算的结果,不是错误”:返回类型的名称,告诉你一些关于如何构思API的意图(API的意图,而不是实现)。

理想情况下,如果您的API不分配内存,则不应返回指针。

标准中提供可选类型,确保您可以编写更具表现力的API。