`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
的推理是 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
有什么好处?
指针可以为 NULL,也可以不为 NULL。这对你是否意味着什么完全取决于你。在某些情况下,nullptr
是您处理的有效值,而在其他情况下,它可以用作指示 "no value, move along".
的标志
有了std::optional
,就有了"contains a value"和"doesn't contain a value"的显式定义。您甚至可以使用可选的指针类型!
这是一个人为的例子:
我有一个名为 Person
的 class,我想从磁盘延迟加载它们的数据。我需要指出是否已加载某些数据。让我们为此使用一个指针:
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()
可能returnnullptr
这个人。我们真的每次有人请求这个名字时都想去磁盘吗?
输入std::optional
。现在我们可以跟踪我们是否已经尝试加载名称 和 如果该名称为空。如果没有 std::optional
,一个解决方案是为名称创建一个布尔值 isLoaded
,实际上每个可选字段。 (如果我们 "just encapsulated the flag into a struct" 怎么办?好吧,那么你已经实现了 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 以较小的规模
展示了这个概念
What is it that we win by adding std::optional over just using std::shared_ptr in general?
假设您需要 return 来自带有标志“不是值”的函数的符号。如果你为此使用 std::shared_ptr
,你将有巨大的开销 - char
将分配在动态内存中,另外 std::shared_ptr
将维护控制块。而 std::optional 在另一边:
If an optional contains a value, the value is guaranteed to be
allocated as part of the optional object footprint, i.e. no dynamic
memory allocation ever takes place. Thus, an optional object models an
object, not a pointer, even though the operator*() and operator->()
are defined.
因此不涉及动态内存分配,即使与原始指针相比,差异也可能很大。
重要的是,如果您尝试从不存在的可选对象访问 value()
,您会得到一个已知的、可捕获的异常,而不是 未定义的行为。因此,如果使用 optional
出现问题,与使用 shared_ptr
或类似方法相比,您可能有更好的调试时间。 (请注意,optional
上的 *
解引用运算符在这种情况下仍会给出 UB;使用 value()
是更安全的选择。
此外,value_or
等方法也很方便,可以让您轻松指定 "default" 值。比较:
(t == nullptr) ? "default" : *t
和
t.value_or("default")
后者可读性更强,也更短。
最后,optional
中的项目存储在对象内。这意味着如果对象不存在,optional
需要比指针更多的存储空间;然而,这也意味着不需要动态分配将对象放入空 optional
.
What is it that we win by adding std::optional over just using std::shared_ptr in general?
@Slava 提到了不执行内存分配的优点,但这是一个附带的好处(好吧,在某些情况下这可能是一个重要的好处,但我的观点是,它不是主要的)。
主要好处是(恕我直言)语义更清晰:
返回指针通常意味着(在现代 C++ 中)"allocates memory",或 "handles memory",或 "knows the address in memory of this and that"。
返回一个可选值意味着"not having a result of this computation, is not an error":return类型的名称,告诉你一些关于API是如何构思的(API的意图,而不是实施)。
理想情况下,如果您的 API 不分配内存,它不应该 return 一个指针。
标准中提供可选类型,确保您可以编写更具表现力的 APIs。
可选的是可以为 null 的值类型。
A shared_ptr
是一个可以为 null 的引用计数引用类型。
A unique_ptr
是一个可以为 null 的仅移动引用类型。
它们的共同点是它们可以为空——它们可以是 "absent".
不同的是,一个是引用类型,一个是值类型。
值类型有几个优点。首先,它不需要在堆上进行分配——它可以与其他数据一起存储。这消除了可能的异常来源(内存分配失败),可以更快(堆比堆栈慢),并且对缓存更友好(因为堆往往相对随机排列)。
引用类型还有其他优点。移动引用类型不需要移动源数据。
对于非移动引用类型,您可以有多个不同名称的相同数据引用。两个不同名称的不同值类型 always 引用不同的数据。无论哪种方式,这都可能是优势或劣势;但它确实使 关于 值类型的推理变得容易得多。
关于 shared_ptr
的推理非常困难。除非对其使用方式进行非常严格的控制,否则几乎不可能知道数据的生命周期是多少。关于 unique_ptr
的推理要容易得多,因为您只需要跟踪它移动的位置。关于 optional
的生命周期的推理是微不足道的(好吧,就像你将它嵌入其中一样微不足道)。
可选接口增加了一些类似 monadic 的方法(如 .value_or
),但这些方法通常可以很容易地添加到任何可为 null 的类型。不过,目前,它们是 optional
而不是 shared_ptr
或 unique_ptr
。
可选的另一个很大的好处是,您非常清楚地希望它有时可以为空。 C++ 中有一个坏习惯,即假定指针和智能指针不为空,因为使用它们的原因 其他 而不是可为空。
所以代码假定一些共享或唯一的 ptr 永远不会为空。并且通常有效。
相比之下,如果你有一个可选的,你拥有它的唯一原因是因为它有可能实际上是空的。
实际上,我对将 unique_ptr<enum_flags> = nullptr
作为参数持怀疑态度,我想说 "these flags are optional",因为在调用者上强制进行堆分配似乎很粗鲁。但是 optional<enum_flags>
不会将此强加给调用者。 optional
的便宜让我愿意在很多情况下使用它,如果我唯一的可空类型是智能指针,我会找到其他一些解决方法。
这消除了 "flag values" 的大部分诱惑,例如 int rows=-1;
。 optional<int> rows;
具有更清晰的含义,并且在调试中会告诉我何时使用行而不检查 "empty" 状态。
可以合理地失败或不失败的函数 return 任何感兴趣的事情都可以避免标志值或堆分配,并且 return optional<R>
。举个例子,假设我有一个可放弃的线程池(比如,一个在用户关闭应用程序时停止处理的线程池)。
我可以从 "queue task" 函数 return std::future<R>
并使用异常来指示线程池已被放弃。但这意味着线程池的所有使用都必须针对 "come from" 异常代码流进行审核。
相反,我可以 return std::future<optional<R>>
,并提示用户他们必须在他们的逻辑中处理 "what happens if the process never happened"。
"Come from" 异常仍然会发生,但它们现在是异常的,不是标准关机程序的一部分。
在其中一些情况下,expected<T,E>
成为标准后将是更好的解决方案。
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
有什么好处?
指针可以为 NULL,也可以不为 NULL。这对你是否意味着什么完全取决于你。在某些情况下,nullptr
是您处理的有效值,而在其他情况下,它可以用作指示 "no value, move along".
有了std::optional
,就有了"contains a value"和"doesn't contain a value"的显式定义。您甚至可以使用可选的指针类型!
这是一个人为的例子:
我有一个名为 Person
的 class,我想从磁盘延迟加载它们的数据。我需要指出是否已加载某些数据。让我们为此使用一个指针:
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()
可能returnnullptr
这个人。我们真的每次有人请求这个名字时都想去磁盘吗?
输入std::optional
。现在我们可以跟踪我们是否已经尝试加载名称 和 如果该名称为空。如果没有 std::optional
,一个解决方案是为名称创建一个布尔值 isLoaded
,实际上每个可选字段。 (如果我们 "just encapsulated the flag into a struct" 怎么办?好吧,那么你已经实现了 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 以较小的规模
What is it that we win by adding std::optional over just using std::shared_ptr in general?
假设您需要 return 来自带有标志“不是值”的函数的符号。如果你为此使用 std::shared_ptr
,你将有巨大的开销 - char
将分配在动态内存中,另外 std::shared_ptr
将维护控制块。而 std::optional 在另一边:
If an optional contains a value, the value is guaranteed to be allocated as part of the optional object footprint, i.e. no dynamic memory allocation ever takes place. Thus, an optional object models an object, not a pointer, even though the operator*() and operator->() are defined.
因此不涉及动态内存分配,即使与原始指针相比,差异也可能很大。
重要的是,如果您尝试从不存在的可选对象访问 value()
,您会得到一个已知的、可捕获的异常,而不是 未定义的行为。因此,如果使用 optional
出现问题,与使用 shared_ptr
或类似方法相比,您可能有更好的调试时间。 (请注意,optional
上的 *
解引用运算符在这种情况下仍会给出 UB;使用 value()
是更安全的选择。
此外,value_or
等方法也很方便,可以让您轻松指定 "default" 值。比较:
(t == nullptr) ? "default" : *t
和
t.value_or("default")
后者可读性更强,也更短。
最后,optional
中的项目存储在对象内。这意味着如果对象不存在,optional
需要比指针更多的存储空间;然而,这也意味着不需要动态分配将对象放入空 optional
.
What is it that we win by adding std::optional over just using std::shared_ptr in general?
@Slava 提到了不执行内存分配的优点,但这是一个附带的好处(好吧,在某些情况下这可能是一个重要的好处,但我的观点是,它不是主要的)。
主要好处是(恕我直言)语义更清晰:
返回指针通常意味着(在现代 C++ 中)"allocates memory",或 "handles memory",或 "knows the address in memory of this and that"。
返回一个可选值意味着"not having a result of this computation, is not an error":return类型的名称,告诉你一些关于API是如何构思的(API的意图,而不是实施)。
理想情况下,如果您的 API 不分配内存,它不应该 return 一个指针。
标准中提供可选类型,确保您可以编写更具表现力的 APIs。
可选的是可以为 null 的值类型。
A shared_ptr
是一个可以为 null 的引用计数引用类型。
A unique_ptr
是一个可以为 null 的仅移动引用类型。
它们的共同点是它们可以为空——它们可以是 "absent".
不同的是,一个是引用类型,一个是值类型。
值类型有几个优点。首先,它不需要在堆上进行分配——它可以与其他数据一起存储。这消除了可能的异常来源(内存分配失败),可以更快(堆比堆栈慢),并且对缓存更友好(因为堆往往相对随机排列)。
引用类型还有其他优点。移动引用类型不需要移动源数据。
对于非移动引用类型,您可以有多个不同名称的相同数据引用。两个不同名称的不同值类型 always 引用不同的数据。无论哪种方式,这都可能是优势或劣势;但它确实使 关于 值类型的推理变得容易得多。
关于 shared_ptr
的推理非常困难。除非对其使用方式进行非常严格的控制,否则几乎不可能知道数据的生命周期是多少。关于 unique_ptr
的推理要容易得多,因为您只需要跟踪它移动的位置。关于 optional
的生命周期的推理是微不足道的(好吧,就像你将它嵌入其中一样微不足道)。
可选接口增加了一些类似 monadic 的方法(如 .value_or
),但这些方法通常可以很容易地添加到任何可为 null 的类型。不过,目前,它们是 optional
而不是 shared_ptr
或 unique_ptr
。
可选的另一个很大的好处是,您非常清楚地希望它有时可以为空。 C++ 中有一个坏习惯,即假定指针和智能指针不为空,因为使用它们的原因 其他 而不是可为空。
所以代码假定一些共享或唯一的 ptr 永远不会为空。并且通常有效。
相比之下,如果你有一个可选的,你拥有它的唯一原因是因为它有可能实际上是空的。
实际上,我对将 unique_ptr<enum_flags> = nullptr
作为参数持怀疑态度,我想说 "these flags are optional",因为在调用者上强制进行堆分配似乎很粗鲁。但是 optional<enum_flags>
不会将此强加给调用者。 optional
的便宜让我愿意在很多情况下使用它,如果我唯一的可空类型是智能指针,我会找到其他一些解决方法。
这消除了 "flag values" 的大部分诱惑,例如 int rows=-1;
。 optional<int> rows;
具有更清晰的含义,并且在调试中会告诉我何时使用行而不检查 "empty" 状态。
可以合理地失败或不失败的函数 return 任何感兴趣的事情都可以避免标志值或堆分配,并且 return optional<R>
。举个例子,假设我有一个可放弃的线程池(比如,一个在用户关闭应用程序时停止处理的线程池)。
我可以从 "queue task" 函数 return std::future<R>
并使用异常来指示线程池已被放弃。但这意味着线程池的所有使用都必须针对 "come from" 异常代码流进行审核。
相反,我可以 return std::future<optional<R>>
,并提示用户他们必须在他们的逻辑中处理 "what happens if the process never happened"。
"Come from" 异常仍然会发生,但它们现在是异常的,不是标准关机程序的一部分。
在其中一些情况下,expected<T,E>
成为标准后将是更好的解决方案。