繁体   English   中英

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

[英]What's the advantage of `std::optional` over `std::shared_ptr` and `std::unique_ptr`?

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

例如,这里的工厂,如果不满足某些条件,则不会构造对象:

#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赢得什么?

我们通过添加 std::optional 而不是一般只使用 std::shared_ptr 会赢得什么?

假设您需要从带有“非值”标志的函数中返回一个符号。 如果您为此使用std::shared_ptr ,则会产生巨大的开销 - char将在动态内存中分配,加上std::shared_ptr将维护控制块。 std::optional在另一边:

如果一个可选项包含一个值,则保证该值作为可选对象占用空间的一部分进行分配,即永远不会发生动态内存分配。 因此,即使定义了 operator*() 和 operator->() ,可选对象也建模对象,而不是指针。

因此不涉及动态内存分配,甚至与原始指针相比的差异也可能很大。

可选是可空值类型。

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

unique_ptr是可以为空的仅移动引用类型。

它们的共同点是它们可以为空——它们可以“不存在”。

它们的不同之处在于,两个是引用类型,另一个是值类型。

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

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

对于仅限非移动的引用类型,您可以通过不同的名称对同一数据进行多个引用。 具有不同名称的两种不同的值类型总是引用不同的数据。 无论哪种方式,这都可能是优势或劣势; 但它确实使有关值类型的推理变得更加容易。

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

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

optional 的另一个巨大好处是,您非常清楚有时希望它可以为空。 有C ++中的坏习惯推测指针和智能指针不为空,因为它们用于比做为空的其他原因。

所以代码假设一些共享或唯一的 ptr 永远不会为空。 它通常有效。

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

在实践中,我对将unique_ptr<enum_flags> = nullptr作为参数持怀疑态度,我想说“这些标志是可选的”,因为强制调用者分配堆似乎很粗鲁。 但是optional<enum_flags>不会强制调用者执行此操作。 optional廉价性使我愿意在许多情况下使用它,如果我唯一的可为空类型是智能指针,我会找到其他一些解决方法。

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

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

我可以从“队列任务”函数返回std::future<R>并使用异常来指示线程池已被放弃。 但这意味着线程池的所有使用都必须针对“来自”异常代码流进行审计。

相反,我可以返回std::future<optional<R>> ,并提示用户他们必须在他们的逻辑中处理“如果过程从未发生过会发生什么”。

“来自”异常仍然可能发生,但它们现在是异常的,不是标准关闭程序的一部分。

在其中一些情况下, expected<T,E>将是一个更好的解决方案,一旦它在标准中。

指针可能是也可能不是 NULL。 这对你来说是否意味着什么完全取决于你。 在某些情况下, nullptr是您处理的有效值,而在其他情况下,它可以用作指示“没有值,继续前进”的标志。

使用std::optional ,有“包含值”和“不包含值”的明确定义。 您甚至可以使用带有 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允许我们检查它。 我在评论中写了一个小例子,以较小的规模展示这个概念

重要的是,如果您尝试从不存在的可选项中访问value() ,则会得到一个已知的、可捕获的异常而不是未定义的行为 因此,如果optional出现问题,与使用shared_ptr等相比,您的调试时间可能要好得多。 (请注意,在这种情况下, optional上的*取消引用运算符仍然给出 UB;使用value()是更安全的选择)。

此外,还有value_or等方法的一般便利性,它允许您很容易地指定“默认”值。 相比:

(t == nullptr) ? "default" : *t

t.value_or("default")

后者更易读,而且略短。

最后, optional项目的存储在对象内。 这意味着如果对象不存在,则可optional需要比指针更多的存储空间; 然而,这也意味着不需要动态分配将对象放入空的optional

我们通过添加 std::optional 而不是一般只使用 std::shared_ptr 会赢得什么?

@Slava 提到了不执行内存分配的优势,但这是一个附带的好处(好吧,在某些情况下它可能是一个显着的好处,但我的意思是,它不是主要的好处)。

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM