Chapter15 std::optional<>
在编程时,我们经常会遇到 可能 会返回/传递/使用一个确定类型对象的场景。
也就是说,这个对象可能有一个确定类型的值也可能没有任何值。
因此,我们需要一种方法来模拟类似指针的语义:指针可以通过nullptr来表示没有值。
解决方法是定义该对象的同时再定义一个附加的bool类型的值作为标志来表示该对象是否有值。
std::optional<>提供了一种类型安全的方式来实现这种对象。
可选对象所需的内存等于 内含 对象的大小加上一个bool类型的大小。 因此,可选对象一般比内含对象大一个字节(可能还要加上内存对齐的空间开销)。 可选对象不需要分配堆内存,并且对齐方式和内含对象相同。
然而,可选对象并不是简单的等价于附加了bool标志的内含对象。 例如,在没有值的情况下,将不会调用内含对象的构造函数(通过这种方式, 没有默认构造函数的内含类型也可以处于有效的默认状态)。
和std::variant<>、std::any一样,可选对象有值语义。
也就是说,拷贝操作会被实现为 深拷贝 :将创建一个新的独立对象,
新对象在自己的内存空间内拥有原对象的标记和内含值(如果有的话)的拷贝。
拷贝一个无内含值的std::optional<>的开销很小,
但拷贝有内含值的std::optional<>的开销约等于拷贝内含值的开销。
另外,std::optional<>对象也支持move语义。
15.1 使用std::optional<>
std::optional<>模拟了一个可以为空的任意类型的实例。
它可以被用作成员、参数、返回值等。
15.1.1 可选的返回值
下面的示例程序展示了将std::optional<>用作返回值的一些功能:
#include <optional>#include <string>#include <iostream>// 如果可能的话把string转换为int:std::optional<int> asInt(const std::string& s){try {return std::stoi(s);}catch (...) {return std::nullopt;}}int main(){for (auto s : {"42", " 077", "hello", "0x33"}) {// 尝试把s转换为int,并打印结果:std::optional<int> oi = asInt(s);if (oi) {std::cout << "convert '" << s << "' to int: " << *oi << "\n";}else {std::cout << "can't convert '" << s << "' to int\n";}}}
这段程序包含了一个asInt()函数来把传入的字符串转换为整数。
然而这个操作有可能会失败,因此把返回值定义为std::optional<>,
这样我们可以返回 “无整数值” 而不是约定一个特殊的int值,
或者向调用者抛出异常来表示失败。
因此,当转换成功时我们用stoi()返回的int初始化返回值并返回,
否则会返回std::nullopt来表明没有int值。
我们也可以像下面这样实现相同的行为:
std::optional<int> asInt(const std::string& s){std::optional<int> ret; // 初始化为无值try {ret = std::stoi(s);}catch (...) {}return ret;}
在main()中,我们用不同的字符串调用了这个函数:
for (auto s : {"42", " 077", "hello", "0x33"} ) {// 尝试把s转换为int,并打印结果:std::optional<int> oi = asInt(s);...}
对于返回的std::optional<int>类型的oi,
我们可以判断它是否含有值(将该对象用作布尔表达式)并通过“解引用”的方式访问了该可选对象的值:
if (oi) {std::cout << "convert '" << s << "' to int: " << *oi << "\n";}
注意用字符串"0x33"调用asInt()将会返回0,
因为stoi()不会以十六进制的方式来解析字符串。
还有一些别的方式来处理返回值,例如:
std::optional<int> oi = asInt(s);if (oi.has_value()) {std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";}
这里使用了has_value()来检查是否返回了一个值,
还使用了value()来访问值。value()比运算符*更安全:
当没有值时它会抛出一个异常。运算符*应该只用于已经确定含有值的场景,
否则程序将可能有未定义的行为。
注意,我们现在可以使用新的类型std::string_view和新的快捷函数std::from_chars()来改进asInt()。
15.1.2 可选的参数和数据成员
另一个使用std::optional<>的例子是传递可选的参数和
设置可选的数据成员:
#include <string>#include <optional>#include <iostream>class Name{private:std::string first;std::optional<std::string> middle;std::string last;public:Name (std::string f, std::optional<std::string> m, std::string l): first{std::move(f)}, middle{std::move(m)}, last{std::move(l)} {}friend std::ostream& operator << (std::ostream& strm, const Name& n) {strm << n.first << ' ';if (n.middle) {strm << *n.middle << ' ';}return strm << n.last;}};int main(){Name n{"Jim", std::nullopt, "Knopf"};std::cout << n << '\n';Name m{"Donald", "Ervin", "Knuth"};std::cout << m << '\n';}
类Name代表了一个由名、可选的中间名、姓组成的姓名。
成员middle被定义为可选的,当没有中间名时可以传递一个std::nullopt,
这和中间名是空字符串是不同的状态。
注意和通常值语义的类型一样,最佳的定义构造函数的方式是以值传参,然后把参数的值移动到成员里。
注意std::optional<>改变了成员middle的值的使用方式。
直接使用n.middle将是一个布尔表达式,表示是否有中间名。
使用*n.middle可以访问当前的值(如果有值的话)。
另一个访问值的方法是使用成员函数value_or(),当没有值的时候可以指定一个备选值。
例如,在类Name里我们可以实现为:
std::cout << middle.value_or(""); // 打印中间名或空
然而,这种方式下,当没有值时名和姓之间将有两个空格而不是一个。
15.2 std::optional<>类型和操作
这一节详细描述std::optional类型和支持的操作。
15.2.1 std::optional<>类型
标准库在头文件<optional>中以如下方式定义了std::optional<>类:
namespace std {template<typename T> class optional;}
另外还定义了下面这些类型和对象:
std::nullopt_t类型的std::nullopt,作为可选对象无值时候的“值”。- 从
std::exception派生的std::bad_optional_access异常类, 当无值时候访问值将会抛出该异常。
可选对象还使用了<utility>头文件中定义的std::in_place对象
(类型是std::in_place_t)来支持用多个参数初始化可选对象(见下文)。
15.2.2 std::optional<>的操作
表std::optional的操作列出了std::optional<>的所有操作:
| 操作符 | 效果 |
|---|---|
| 构造函数 | 创建一个可选对象(可能会调用内含类型的构造函数也可能不会) |
make_optional<>() |
创建一个用参数初始化的可选对象 |
| 析构函数 | 销毁一个可选对象 |
= |
赋予一个新值 |
emplace() |
给内含类型赋予一个新值 |
reset() |
销毁值(使对象变为无值状态) |
has_value() |
返回可选对象是否含有值 |
转换为bool |
返回可选对象是否含有值 |
* |
访问内部的值(如果无值将会产生未定义行为) |
-> |
访问内部值的成员(如果无值将会产生未定义行为) |
value() |
访问内部值(如果无值将会抛出异常) |
value_or() |
访问内部值(如果无值将返回参数的值) |
swap() |
交换两个对象的值 |
==、!=、<、<=、>、>= |
比较可选对象 |
hash<> |
计算哈希值的函数对象的类型 |
构造函数
特殊的构造函数允许你直接传递内含类型的值作为参数。
- 你可以创建一个不含有值的可选对象。这种情况下,你必须指明内含的类型:
这种情况下将不会调用内含类型的任何构造函数。std::optional<int> o1;std::optional<int> o2(std::nullopt);
- 你可以传递一个值来初始化内含类型。得益于推导指引,你不需要再指明内含类型:
std::optional o3{42}; // 推导出optional<int>std::optional o4{"hello"}; // 推导出optional<const char*>using namespace std::string_literals;std::optional o5{"hello"s}; // 推导出optional<string>
- 为了用多个参数初始化可选对象,你必须传递一个构造好的对象或者添加
std::in_place作为第一个参数(这种情况下推导不出内含类型):
注意第二种方式避免了创建临时变量。通过使用这种方式,你甚至可以传递一个初值列加上其他参数:std::optional o6{std::complex{3.0, 4.0}};std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};
然而,只有当所有的初始值都和容器里元素的类型匹配时才可以这么写。否则, 你必须显式传递一个// 用lambda作为排序准则初始化set:auto sc = [] (int x, int y) {return std::abs(x) < std::abs(y);};std::optional<std::set<int, decltype(sc)>> o8{std::in_place, {4, 8, -7, -2, 0, 5}, sc};
std::initializer_list<>:// 用lambda作为排序准则初始化setauto sc = [] (int x, int y) {return std::abs(x) < std::abs(y);};std::optional<std::set<int, decltype(sc)>> o8{std::in_place,std::initializer_list<int>{4, 5L}, sc};
- 如果底层类型支持拷贝的话可选对象也可以拷贝(支持类型转换):
然而,注意如果内含类型本身可以用一个可选对象来构造,那么将会优先用可选对象构造内含对象,而不是拷贝:std::optional o9{"hello"}; // 推导出optional<const char*>std::optional<std::string> o10{o9}; // OK
std::optional<int> o11;std::optional<std::any> o12{o11}; // o12内含了一个any对象,该对象的值是一个空的optional<int>
注意还有一个快捷函数make_optional<>(),它可以用单个或多个参数初始化一个可选对象
(不需要再使用in_place参数)。
像通常的make...函数一样,它的参数也会退化:
auto o13 = std::make_optional(3.0); // optional<double>auto o14 = std::make_optional("hello"); // optional<const char*>auto o15 = std::make_optional<std::complex<double>>(3.0, 4.0);
然而,注意没有一个构造函数可以根据参数的值来判断是应该用某个值还是用nullopt
来初始化可选对象。这种情形下,只能使用运算符?:。
例如:
std::multimap<std::string, std::string> englishToGerman;...auto pos = englishToGerman.find("wisdom");auto o16 = pos != englishToGerman.end() ? std::optional{pos->second} : std::nullopt;
这个例子中,根据类模板参数推导,通过表达式std::optional{pos->second}能推导出
o16的类型是std::optional<std::string>。
类模板参数推导不能对单独的std::nullopt生效,但通过使用运算符?:,
std::nullopt也会自动转换成std::optional<string>类型,
这是因为?:运算符的两个表达式必须有相同的类型。
访问值
为了检查一个可选对象是否有值,你可以调用has_value()或者在bool表达式中使用它:
std::optional o{42};if (o) ... // trueif (!o) ... // falseif (o.has_value())... // true
没有为可选对象定义I/O运算符,因为当可选对象无值时不确定应该输出什么:
std::cout << o; // ERROR
要访问内部值可以使用指针语法。也就是说,通过运算符*,你可以直接访问可选对象的内部值,
也可以使用->访问内部值的成员:
std::optional o{std::pair{42, "hello"}};auto p = *o; // 初始化p为pair<int, string>std::cout << o->first; // 打印出42
注意这些操作符都需要可选对象内含有值。在没有值的情况下这样使用会导致未定义行为:
std::optional<std::string> o{"hello"};std::cout << *o; // OK:打印出"hello"o = std::nullopt;std::cout << *o; // 未定义行为
注意在实践中第二个输出语句仍能正常编译并可能再次打印出"hello",
因为可选对象里底层值的内存并没有被修改。然而,你绝不应该依赖这一点。
如果你不知道一个可选对象是否有值,你必须像下面这样调用:
if (o) std::cout << *o; // OK(可能输出为空字符串)
或者你可以使用value()成员函数来访问值,
当内部没有值时将抛出一个std::bad_optional_access异常:
std::cout << o.value(); // OK(无值时会抛出异常)
std::bad_optional_access直接派生自std::exception。
请注意operator*和value()都是返回内含对象的引用。
因此,当直接使用这些操作返回的临时对象时要小心。例如,对于一个返回可选字符串的函数:
std::optional<std::string> getString();
把它返回的可选对象的值赋给新对象总是安全的:
auto a = getString().value(); // OK:内含对象的拷贝或抛出异常
然而,直接使用返回值(或者作为参数传递)是麻烦的根源:
auto b = *getString(); // ERROR:如果返回std::nullopt将会有未定义行为const auto& r1 = getString().value(); // ERROR:引用销毁的内含对象auto&& r2 = getString().value(); // ERROR:引用销毁的内含对象
使用引用的问题是:根据规则,引用会延长value()的返回值的生命周期,
而不是 getString()返回的可选对象的生命周期。
因此,r1和r2会引用不存在的值,使用它们将会导致未定义行为。
注意当使用范围for循环时很容易出现这个问题:
std::optional<std::vector<int>> getVector();...for (int i : getVector().value()) { // ERROR:迭代一个销毁的vectorstd::cout << i << '\n';}
注意迭代一个non-optional的vector<int>类型的返回值是可以的。
因此,不要盲目的把函数返回值替换为相应的可选对象类型。
(译者注:有点看不懂原文这里想表达什么意思,暂且就这么翻译。)
最后,你可以在获取值时针对无值的情况设置一个fallback值。 这通常是把一个可选对象写入到输出流的最简单的方式:
std::cout << o.value_or("NO VALUE"); // OK(没有值时写入NO VALUE)
然而,value()和value_or()之间有一个需要考虑的差异:
value_or()返回值,而value()返回引用。
这意味着如下调用:
std::cout << middle.value_or("");
和:
std::cout << o.value_or("fallback");
都会暗中分配内存,而value()永远不会。
然而,当在临时对象(rvalue)上调用value_or()时,
将会 移动 走内含对象的值并以值返回,而不是调用拷贝函数构造。
这是唯一一种能让value_or()适用于move-only的类型的方法,
因为在左值(lvalue)上调用的value_or()的重载版本需要内含对象可以拷贝。
因此,上面例子中效率最高的实现方式是:
std::cout << o ? o->c_str() : "fallback";
而不是:
std::cout << o.value_or("fallback");
value_or()是一个能够更清晰地表达意图的接口,但开销可能会更大一点。
比较
你可以使用通常的比较运算符。操作数可以是可选对象、内含类型的对象、std::nullopt。
- 如果两个操作数都是有值的对象,将会调用内含类型的相应操作符。
- 如果两个操作数都是没有值的对象,那么它们相等(
==、<=、>=返回true, 其他比较返回false)。 - 如果恰有一个操作数有值,那么无值的操作数小于有值的操作数。
例如:
std::optional<int> o0;std::optional<int> o1{42};o0 == std::nullopt // 返回trueo0 == 42 // 返回falseo0 < 42 // 返回trueo0 > 42 // 返回falseo1 == 42 // 返回trueo0 < o1 // 返回true
这意味着unsigned int类型的可选对象,甚至可能小于0:
std::optional<unsigned> uo;uo < 0 // 返回trueuo < -42 // 返回true
对于bool类型的可选对象,也可能小于false:
std::optional<bool> bo;bo < false // 返回true
为了让代码可读性更强,应该使用
if (!uo.has_value())
而不是
if (uo < 0)
可选对象和底层类型之间的混合比较也是支持的,前提是底层类型支持这种比较:
std::optional<int> o1{42};std::optional<double> o2{42.0};o2 == 42 // 返回trueo1 == o2 // 返回true
如果底层类型支持隐式类型转换,那么相应的可选对象类型也会进行隐式类型转换。
注意可选的bool类型或原生指针可能会导致一些奇怪的行为。
修改值
赋值运算和emplace()操作可以用来修改值:
std::optional<std::complex<double>> o; // 没有值std::optional ox{77}; // optional<int>,值为77o = 42; // 值变为complex(42.0, 0.0)o = {9.9, 4.4}; // 值变为complex(9.9, 4.4)o = ox; // OK,因为int转换为complex<double>o = std::nullopt; // o不再有值o.emplace(5.5, 7.7); // 值变为complex(5.5, 7.7)
赋值为std::nullopt会移除内含值,如果之前有值的话将会调用内含类型的析构函数。
你也可以通过调用reset()实现相同的效果:
o.reset(); // o不再有值
或者赋值为空的花括号:
o = {}; // o不再有值
最后,我们也可以使用operator*来修改值,因为它返回的是引用。
然而,注意这种方式要求值必须存在:
std::optional<std::complex<double>> o;*o = 42; // 未定义行为...if (o) {*o = 88; // OK:值变为complex(88.0, 0.0)*o = {1.2, 3.4}; // OK:值变为complex(1.2, 3.4)}
move语义
std::optional<>也支持move语义。如果你move了整个可选对象,
那么内部的状态会被拷贝,值会被move。因此,被move的可选对象仍保持原来的状态,但值变为未定义。
然而,你也可以单独把内含的值移进或移出。例如:
std::optional<std::string> os;std::string s = "a very very very long string";os = std::move(s); // OK,movestd::string s2 = *os; // OK,拷贝std::string s3 = std::move(*os); // OK,move
注意在最后一次调用之后,os仍然含有一个字符串值,但就像值被移走的对象一样,
这个值是未定义的。因此,你可以使用它,但不要对它的值有任何假设。你也可以给它赋一个新的字符串。
另外注意有些重载版本会保证临时的可选对象被move。
考虑下面这个返回一个可选字符串的函数:
std::optional<std::string> func();
在这种情况下,下面的代码将会move临时可选对象的值:
std::string s4 = func().value(); // OK,movestd::string s5 = *func(); // OK,move
可以通过重载相应成员函数的右值版本来保证上述的行为:
namespace std {template<typename T>class optional {...constexpr T& operator*() &;constexpr const T& operator*() const&;constexpr T&& operaotr*() &&;constexpr const T&& operator*() const&&;constexpr T& value() &;constexpr const T& value() const&;constexpr T&& value() &&;constexpr const T&& value() const&&;};}
换句话说,你也可以像下面这样写:
std::optional<std::string> os;std::string s6 = std::move(os).value(); // OK,move
哈希
可选对象的哈希值就等于内含值的哈希值(如果有值的话)。
无值的可选对象的哈希值未定义。
15.3 特殊情况
一些特定的可选类型可能会导致特殊或意料之外的行为。
15.3.1 bool类型或原生指针的可选对象
将可选对象用作bool值时使用比较运算符会有特殊的语义。
如果内含类型是bool或者指针类型的话这可能导致令人迷惑的行为。例如:
std::optional<bool> ob{false}; // 值为falseif (!ob) ... // 返回falseif (ob == false) ... // 返回truestd::optional<int*> op{nullptr};if (!op) ... // 返回falseif (op == nullptr) ... // 返回true
15.3.2 可选对象的可选对象
理论上讲,你可以定义可选对象的可选对象:
std::optional<std::optional<std::string>> oos1;std::optional<std::optional<std::string>> oos2 = "hello";std::optional<std::optional<std::string>> oos3{std::in_place, std::in_place, "hello"};std::optional<std::optional<std::complex<double>>> ooc{std::in_place, std::in_place, 4.2, 5.3};
你甚至可以通过隐式类型转换直接赋值:
oos1 = "hello"; // OK:赋新值ooc.emplace(std::in_place, 7.2, 8.3);
因为两层可选对象都可能没有值, 可选对象的可选对象允许你在内层无值或者在外层无值,这可能会导致不同的语义:
*oos1 = std::nullopt; // 内层可选对象无值oos1 = std::nullopt; // 外层可选对象无值
这意味着在处理这种可选对象的时候你必须特别小心:
if (!oos1) std::cout << "no value\n";if (oos1 && !*oos1) std::cout << "no inner value\n";if (oos1 && *oos1) std::cout << "value: " << **oos1 << '\n';
然而,从语义上来看,这只是一个有两种状态都代表无值的类型而已。
因此,带有两个bool值或monostate的std::variant<>将是一个更好的替代。
