一个可选类型是一个可空的值类型。
shared_ptr是一种可计数的可空引用类型。
unique_ptr是一种只能移动但可空的引用类型。
它们共同之处在于它们是可空的-它们可以“不存在”。
它们之间的不同在于两个是引用类型,而另一个是值类型。
值类型有几个优点。首先,它不需要在堆上分配——它可以与其他数据一起存储。这消除了可能的异常源(内存分配失败),可以更快(堆比栈慢得多),并且更加缓存友好(因为堆倾向于相对随机地排列)。
引用类型具有其他优点。移动引用类型不需要移动源数据。
对于非移动引用类型,可以通过不同的名称具有对同一数据的多个引用。两个具有不同名称的不同值类型始终引用不同的数据。这可以是优势或劣势,但它确实使对值类型的推理容易得多。
推理shared_ptr极其困难。除非对使用它的方式施加非常严格的控制,否则几乎不可能知道数据的生命周期是什么时候。推理unique_ptr要容易得多,因为您只需要追踪它被移动到哪里。推理optional的生命周期很简单(嗯,就像您将其嵌入的那样简单)。
optional接口已增加了一些类似于单子的方法(如.value_or),但这些方法通常可以轻松地添加到任何可空类型中。尽管如此,目前它们适用于optional而不是shared_ptr或unique_ptr。
optional的另一个重大好处是它非常清楚您希望它有时为空。在C++中存在一种不良习惯,即假定指针和智能指针不为null,因为它们用于其他原因而不是可空。
因此,代码假设某个共享或唯一指针永远不会为null。通常情况下它也有效。
相比之下,如果您有一个optional,那么唯一的原因是因为它实际上可能为null。
在实践中,我对于以“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>
将是更好的解决方案,一旦它进入标准。
std::optional
做得有些过头了。以前我在与我的博士导师进行激烈争论的时候,他总是说 C 比 C++ 更好,因为你可以通过一本 300 页的书学习 C。 - The Quantum Physiciststd::optional
而不是std::shared_ptr
的更好的推理是其中一个答案,并且对我来说很有意义,因为不需要动态分配。顺便说一下,我总是不同意我的导师,我不认为C语言“更好”,但我只是在说它对外行人看起来是什么样子(我目前是C和C++开发人员)。此外,我认为与typedef的比较并不公平。我认为像size_t
这样的typedef非常有用,可以实现向前兼容,并且不像std::optional
那样是语言中的新构造。 - The Quantum Physiciststd::optional
不是一种新的语言结构,它只是一个标准库类型,类似于std::string
或std::size_t
。(顺便说一句,我推荐您观看由Null引用发明者Tony Hoare所演讲的“空引用:价值数十亿美元的错误”视频。) - molbdnilo