nullptr
来表示没有值。解决方法是定义该对象的同时再定义一个附加的bool类型的值作为标志来表示该对象是否有值。std::optional<>
提供了一种类型安全的方式来实现这种对象。bool
类型的大小。因此,可选对象一般比内含对象大一个字节(可能还要加上内存对齐的空间开销)。**可选对象不需要分配堆内存,并且对齐方式和内含对象相同。**然而,可选对象并不是简单的等价于附加了bool
标志的内含对象。例如,在没有值的情况下,将不会调用内含对象的构造函数(通过这种方式,没有默认构造函数的内含类型也可以处于有效的默认状态)。While you can achieve “null-ability” by using unique values (-1, infinity, nullptr), it’s not as clear as the separate wrapper type. Alternatively, you could even use std::unique_ptr and treat the empty pointer as not initialized - this works, but comes with the cost of allocating memory for the object.
虽然您可以通过使用唯一值(-1, infinity, nullptr)来实现“空能力”,但它并不像单独的包装器类型那样清晰。或者,您甚至可以使用 std::unique_ptr 并将空指针视为未初始化 - 这有效,但会带来为对象分配内存的成本。
std::variant<>
、std::any
一样,可选对象有值语义。也就是说,拷贝操作会被实现为 深拷贝 :将创建一个新的独立对象,新对象在自己的内存空间内拥有原对象的标记和内含值(如果有的话)的拷贝。拷贝一个无内含值的std::optional<>
的开销很小,但拷贝有内含值的std::optional<>
的开销约等于拷贝内含值的开销。另外,std::optional<>
对象也支持move语义。通常,可以在以下情况下使用可选包装器:
std::optional
您可以获得更多信息。std::optional
(您可以在系统中传递它),然后仅在以后需要时才加载。boost::optional
类型的描述很好,它总结了我们何时应该使用这种类型, When to use Optional
建议在以下情况下使用
optional
:只有一个(对所有各方)明确的原因导致没有类型T
的值,并且缺少值与具有任何常规T
值一样自然
虽然有时使用optional
的决定可能很模糊,但不应该将其用于错误处理。因为它最适合值为空并且是程序正常状态的情况。
std::optional<>
std::optional<>
模拟了一个可以为空的任意类型的实例。它可以被用作成员、参数、返回值等。
下面的示例程序展示了将std::optional<>
用作返回值的一些功能:
#include
#include
#include
// 如果可能的话把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";
}
}
}
运行结果:
convert '42' to int: 42
convert ' 077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0
预处理代码如下:
#include
#include
#include
// 如果å¯èƒ½çš„è¯æŠŠstring转æ¢ä¸ºint:
std::optional<int> asInt(const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & s)
{
try
{
return std::optional<int>(std::stoi(s, 0, 10));
} catch(...) {
return std::optional<int>(std::nullopt_t(std::nullopt));
}
;
}
int main()
{
{
std::initializer_list<const char *> && __range1 = std::initializer_list<const char *>{"42", " 077", "hello", "0x33"};
const char *const * __begin1 = __range1.begin();
const char *const * __end1 = __range1.end();
for(; __begin1 != __end1; ++__begin1) {
const char * s = *__begin1;
std::optional<int> oi = asInt(std::basic_string<char, std::char_traits<char>, std::allocator<char> >(s, std::allocator<char>()));
if(static_cast<bool>(oi.operator bool())) {
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, "convert '"), s), "' to int: ").operator<<(oi.operator*()), "\n");
} else {
std::operator<<(std::operator<<(std::operator<<(std::cout, "can't convert '"), s), "' to int\n");
}
}
}
return 0;
}
这段程序包含了一个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
类型的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()
比运算符*
更安全:当没有值时它会抛出一个异常。运算符*
应该只用于已经确定含有值的场景,否则程序将可能有未定义的行为。
完整示例如下:
#include
#include
#include
std::optional<int> asInt1(const std::string& s) {
std::optional<int> ret; // 初始化为无值
try {
ret = std::stoi(s);
} catch (...) {
}
return ret;
}
int main() {
for (auto s : {"42", " 077", "hello", "0x33"}) {
// 尝试把s转换为int,并打印结果:
std::optional<int> oi = asInt1(s);
if (oi) {
std::cout << "convert '" << s << "' to int: " << *oi << "\n";
} else {
std::cout << "can't convert '" << s << "' to int\n";
}
}
std::cout << "--------------" << std::endl;
for (auto s : {"42", " 077", "hello", "0x33"}) {
std::optional<int> oi = asInt1(s);
if (oi.has_value()) {
std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
} else {
std::cout << "can't convert '" << s << "' to int\n";
}
}
}
运行结果:
convert '42' to int: 42
convert ' 077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0
--------------
convert '42' to int: 42
convert ' 077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0
**注意,**我们现在可以使用新的类型std::string_view
和新的快捷函数std::from_chars()
来改进asInt()
。
另一个使用std::optional<>
的例子是传递可选的参数和设置可选的数据成员:
#include
#include
#include
using namespace std;
class Name {
private:
string first;
optional<std::string> middle;
string last;
public:
Name(string f, optional<std::string> m, string l)
: first{std::move(f)}, middle{std::move(m)}, last{std::move(l)} {}
friend ostream& operator<<(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"};
cout << n << endl;
Name m{"Donald", "Ervin", "Knuth"};
cout << m << endl;
}
运行结果:
Jim Knopf
Donald Ervin Knuth
类Name
代表了一个由名、可选的中间名、姓组成的姓名。成员middle
被定义为可选的,当没有中间名时可以传递一个std::nullopt
,这和中间名是空字符串是不同的状态。
注意和通常值语义的类型一样,最佳的定义构造函数的方式是以值传参,然后把参数的值移动到成员里。
注意std::optional<>
改变了成员middle
的值的使用方式。直接使用n.middle
将是一个布尔表达式,表示是否有中间名。使用*n.middle
可以访问当前的值(如果有值的话)。
另一个访问值的方法是使用成员函数value_or()
,当没有值的时候可以指定一个备选值。例如,在类Name
里我们可以实现为:
std::cout << middle.value_or(""); // 打印中间名或空
然而,这种方式下,当没有值时名和姓之间将有两个空格而不是一个。
另外的示例
#include
#include
class UserRecord {
public:
UserRecord(const std::string& name, std::optional<std::string> nick,
std::optional<int> age)
: mName{name}, mNick{nick}, mAge{age} {}
friend std::ostream& operator<<(std::ostream& stream,
const UserRecord& user);
private:
std::string mName;
std::optional<std::string> mNick;
std::optional<int> mAge;
};
std::ostream& operator<<(std::ostream& os, const UserRecord& user) {
os << user.mName << ' ';
if (user.mNick) {
os << *user.mNick << ' ';
}
if (user.mAge) os << "age of " << *user.mAge;
return os;
}
int main() {
UserRecord tim{"Tim", "SuperTim", 16};
UserRecord nano{"Nathan", std::nullopt, std::nullopt};
std::cout << tim << "\n";
std::cout << nano << "\n";
}
运行结果:
Tim SuperTim age of 16
Nathan
std::optional<>
类型和操作std::optional<>
类型标准库在头文件
中以如下方式定义了std::optional<>
类:
namespace std {
template<typename T> class optional;
}
另外还定义了下面这些类型和对象:
std::nullopt_t
类型的std::nullopt
,作为可选对象无值时候的“值”。std::nullopt_t
是空类类型,用于指示 optional
类型拥有未初始化状态。特别是 std::optional
有一个以 nullopt_t
为单参数的构造函数,它创建不含值的optional
。std::exception
派生的std::bad_optional_access
异常类,当无值时候访问值将会抛出该异常。
头文件中定义的std::in_place
对象(类型是std::in_place_t
)来支持用多个参数初始化可选对象。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
std::optional o4{"hello"}; // 推导出optional
using namespace std::string_literals;
std::optional o5{"hello"s}; // 推导出optional
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作为排序准则初始化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,
std::initializer_list<int>{4, 5L}, sc};
std::optional o9{"hello"}; // 推导出optional
std::optional<std::string> o10{o9}; // OK
然而,注意如果内含类型本身可以用一个可选对象来构造,那么将会优先用可选对象构造内含对象,而不是拷贝:
std::optional<int> o11;
std::optional<std::any> o12{o11}; // o12内含了一个any对象,该对象的值是一个空的optional
注意还有一个快捷函数make_optional<>()
,它可以用单个或多个参数初始化一个可选对象(不需要再使用in_place
参数)。像通常的make...
函数一样,它的参数也会退化:
auto o13 = std::make_optional(3.0); // optional
auto o14 = std::make_optional("hello"); // optional
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::nullopt
生效,但通过使用运算符?:
,std::nullopt
也会自动转换成std::optional
类型,这是因为?:
运算符的两个表达式必须有相同的类型。
综合示例:
#include
#include
#include
#include
#include
int main() {
std::optional<int> o1, // 空
o2 = 1, // 从右值初始化
o3 = o2; // 复制构造函数
// 调用 std::string( initializer_list ) 构造函数
std::optional<std::string> o4(std::in_place, {'a', 'b', 'c'});
// 调用 std::string( size_type count, CharT ch ) 构造函数
std::optional<std::string> o5(std::in_place, 3, 'A');
// 从 std::string 移动构造,用推导指引拾取类型
std::optional o6(std::string{"deduction"});
std::cout << *o2 << ' ' << *o3 << ' ' << *o4 << ' ' << *o5 << ' ' << *o6
<< '\n';
// empty:
std::optional<int> oEmpty;
std::optional<float> oFloat = std::nullopt;
// direct:
std::optional<int> oInt(10);
std::optional oIntDeduced(10); // 推到指引
// make_optional
auto oDouble = std::make_optional(3.0);
auto oComplex = std::make_optional<std::complex<double>>(3.0, 4.0);
// in_place
std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};
// will call vector with direct init of {1, 2, 3}
std::optional<std::vector<int>> oVec(std::in_place, {1, 2, 3});
// copy/assign:
auto oIntCopy = oInt;
std::cout << oInt.value() << ' ' << oIntDeduced.value() << ' '
<< oDouble.value() << ' ' << oComplex.value() << ' ' << o7.value()
<< " " << oVec.value().size() << " " << oInt.value() << std::endl;
}
输出结果如下:
1 1 abc AAA deduction
10 10 3 (3,4) (3,4) 3 10
预处理代码如下:
#include
#include
#include
#include
#include
int main()
{
std::optional<int> o1 = std::optional<int>();
std::optional<int> o2 = std::optional<int>(1);
std::optional<int> o3 = std::optional<int>(o2);
std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o4 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::in_place_t(std::in_place), std::initializer_list<char>{'a', 'b', 'c'});
std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o5 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::in_place_t(std::in_place), 3, 'A');
std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o6 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >{"deduction", std::allocator<char>()});
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(o2.operator*()), ' ').operator<<(o3.operator*()), ' '), o4.operator*()), ' '), o5.operator*()), ' '), o6.operator*()), '\n');
std::optional<int> oEmpty = std::optional<int>();
std::optional<float> oFloat = std::optional<float>(std::nullopt_t(std::nullopt));
std::optional<int> oInt = std::optional<int>(10);
std::optional<int> oIntDeduced = std::optional<int>(10);
std::optional<double> oDouble = std::make_optional(3.0);
std::optional<std::complex<double> > oComplex = std::make_optional<std::complex<double> >(3.0, 4.0);
std::optional<std::complex<double> > o7 = std::optional<std::complex<double> >{std::in_place_t(std::in_place), 3.0, 4.0};
std::optional<std::vector<int, std::allocator<int> > > oVec = std::optional<std::vector<int, std::allocator<int> > >(std::in_place_t(std::in_place), std::initializer_list<int>{1, 2, 3});
std::optional<int> oIntCopy = std::optional<int>(oInt);
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(oInt.value()), ' ').operator<<(oIntDeduced.value()), ' ').operator<<(oDouble.value()), ' '), oComplex.value()), ' '), o7.value()), " ").operator<<(oVec.value().size()), " ").operator<<(oInt.value()).operator<<(std::endl);
return 0;
}
示例2
#include
#include
#include
#include
#include
int main() {
auto op1 = std::make_optional<std::vector<char>>({'a', 'b', 'c'});
std::cout << "op1: ";
for (char c : op1.value()) {
std::cout << c << ",";
}
auto op2 = std::make_optional<std::vector<int>>(5, 2);
std::cout << "\nop2: ";
for (int i : *op2) {
std::cout << i << ",";
}
std::string str{"hello world"};
auto op3 = std::make_optional<std::string>(std::move(str));
std::cout << "\nop3: " << quoted(op3.value_or("empty value")) << '\n';
std::cout << "str: " << std::quoted(str) << '\n';
}
运行输出如下:
op1: a,b,c,
op2: 2,2,2,2,2,
op3: "hello world"
str: ""
预处理代码如下:
int main()
{
std::optional<std::vector<char, std::allocator<char> > > op1 = std::make_optional<std::vector<char, std::allocator<char> > >(std::initializer_list<char>{'a', 'b', 'c'});
std::operator<<(std::cout, "op1: ");
{
std::vector<char, std::allocator<char> > & __range1 = op1.value();
__gnu_cxx::__normal_iterator<char *, std::vector<char, std::allocator<char> > > __begin1 = __range1.begin();
__gnu_cxx::__normal_iterator<char *, std::vector<char, std::allocator<char> > > __end1 = __range1.end();
for(; !__gnu_cxx::operator==(__begin1, __end1); __begin1.operator++()) {
char c = __begin1.operator*();
std::operator<<(std::operator<<(std::cout, c), ",");
}
}
std::optional<std::vector<int, std::allocator<int> > > op2 = std::make_optional<std::vector<int, std::allocator<int> > >(5, 2);
std::operator<<(std::cout, "\nop2: ");
{
std::vector<int, std::allocator<int> > & __range1 = op2.operator*();
__gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __begin1 = __range1.begin();
__gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __end1 = __range1.end();
for(; !__gnu_cxx::operator==(__begin1, __end1); __begin1.operator++()) {
int i = __begin1.operator*();
std::operator<<(std::cout.operator<<(i), ",");
}
}
std::basic_string<char, std::char_traits<char>, std::allocator<char> > str = std::basic_string<char, std::char_traits<char>, std::allocator<char> >{"hello world", std::allocator<char>()};
std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > op3 = std::make_optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::move(str));
std::operator<<(std::__detail::operator<<(std::operator<<(std::cout, "\nop3: "), std::quoted(op3.value_or<const char (&)[12]>("empty value"), char('"'), char('\\'))), '\n');
std::operator<<(std::__detail::operator<<(std::operator<<(std::cout, "str: "), std::quoted(str, char('"'), char('\\'))), '\n');
return 0;
}
几种选择:
operator*
和 operator->
- 返回指向所含值的指针或到它的引用。 解引用运算符operator*()
不检查此optional
是否含值,它可能比 value()
更有效率。若 *this
不含值则行为未定义。 value()
- 若*this
含值,则返回到所含值引用。否则,抛出 std::bad_optional_access
异常。value_or(defaultVal)
- 返回值(如果可用),否则返回默认值。为了检查一个可选对象是否有值,你可以调用has_value()
或者在bool
表达式中使用它:
std::optional o{42};
if (o) ... // true
if (!o) ... // false
if (o.has_value())... // true
预处理代码如下:
std::optional<int> o = std::optional<int>{42};
if(static_cast<bool>(o.operator bool())) {
}
if(!static_cast<bool>(o.operator bool())) {
}
if(o.has_value()) {
}
没有为可选对象定义I/O
运算符,因为当可选对象无值时不确定应该输出什么:
std::cout << o; // ERROR
要访问内部值可以使用指针语法。也就是说,通过运算符*
,你可以直接访问可选对象的内部值,也可以使用->
访问内部值的成员:
std::optional o{std::pair{42, "hello"}};
auto p = *o; // 初始化p为pair
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:迭代一个销毁的vector
std::cout << i << '\n';
}
注意迭代一个non-optional的vector
类型的返回值是可以的。因此,不要盲目的把函数返回值替换为相应的可选对象类型。
最后,你可以在获取值时针对无值的情况设置一个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()
是一个能够更清晰地表达意图的接口,但开销可能会更大一点。
示例
#include
#include
#include
int main() {
// by operator*
std::optional<int> oint = 10;
std::cout << "oint " << *oint << '\n';
// by value()
std::optional<std::string> ostr("hello");
try {
std::cout << "ostr " << ostr.value() << '\n';
} catch (const std::bad_optional_access& e) {
std::cout << e.what() << "\n";
}
// by value_or()
std::optional<double> odouble; // empty
std::cout << "odouble " << odouble.value_or(10.0) << '\n';
}
运行结果:
oint 10
ostr hello
odouble 10
预处理代码如下:
#include
#include
#include
int main()
{
std::optional<int> oint = std::optional<int>(10);
std::operator<<(std::operator<<(std::cout, "oint ").operator<<(oint.operator*()), '\n');
std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > ostr = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >("hello");
try
{
std::operator<<(std::operator<<(std::operator<<(std::cout, "ostr "), ostr.value()), '\n');
} catch(const std::bad_optional_access & e) {
std::operator<<(std::operator<<(std::cout, e.what()), "\n");
}
;
std::optional<double> odouble = std::optional<double>();
std::operator<<(std::operator<<(std::cout, "odouble ").operator<<(odouble.value_or<double>(10.0)), '\n');
return 0;
}
你可以使用通常的比较运算符。操作数可以是可选对象、内含类型的对象、std::nullopt
。
==、<=、>=
返回true
,其他比较返回false
)。例如:
std::optional<int> o0;
std::optional<int> o1{42};
o0 == std::nullopt // 返回true
o0 == 42 // 返回false
o0 < 42 // 返回true
o0 > 42 // 返回false
o1 == 42 // 返回true
o0 < o1 // 返回true
这意味着unsigned int
类型的可选对象,甚至可能小于0:
std::optional<unsigned> uo;
uo < 0 // 返回true
uo < -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 // 返回true
o1 == o2 // 返回true
如果底层类型支持隐式类型转换,那么相应的可选对象类型也会进行隐式类型转换。
注意可选的bool
类型或原生指针可能会导致一些奇怪的行为。
示例:
#include
#include
int main()
{
std::optional<int> oEmpty;
std::optional<int> oTwo(2);
std::optional<int> oTen(10);
std::cout << std::boolalpha;
std::cout << (oTen > oTwo) << "\n";
std::cout << (oTen < oTwo) << "\n";
std::cout << (oEmpty < oTwo) << "\n";
std::cout << (oEmpty == std::nullopt) << "\n";
std::cout << (oTen == 10) << "\n";
}
运行结果如下:
true
false
true
true
true
预处理代码如下:
std::optional<int> oEmpty = std::optional<int>();
std::optional<int> oTwo = std::optional<int>(2);
std::optional<int> oTen = std::optional<int>(10);
std::cout.operator<<(std::boolalpha);
std::operator<<(std::cout.operator<<((std::operator>(oTen, oTwo))), "\n");
std::operator<<(std::cout.operator<<((std::operator<(oTen, oTwo))), "\n");
std::operator<<(std::cout.operator<<((std::operator<(oEmpty, oTwo))), "\n");
std::operator<<(std::cout.operator<<((std::operator==(oEmpty, std::nullopt_t(std::nullopt)))), "\n");
std::operator<<(std::cout.operator<<((std::operator==(oTen, 10))), "\n");
赋值运算和emplace()
操作可以用来修改值:
std::optional<std::complex<double>> o; // 没有值
std::optional ox{77}; // optional,值为77
o = 42; // 值变为complex(42.0, 0.0)
o = {9.9, 4.4}; // 值变为complex(9.9, 4.4)
o = ox; // OK,因为int转换为complex
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)
}
示例:
#include
#include
#include
class UserName {
public:
explicit UserName(const std::string& str) : mName(str) {
std::cout << "UserName::UserName(\'";
std::cout << mName << "\')\n";
}
~UserName() {
std::cout << "UserName::~UserName(\'";
std::cout << mName << "\')\n";
}
private:
std::string mName;
};
int main() {
std::optional<UserName> oEmpty;
// emplace:
oEmpty.emplace("Steve");
// calls ~Steve and creates new Mark:
oEmpty.emplace("Mark");
// reset so it's empty again
oEmpty.reset(); // calls ~Mark
// same as:
// oEmpty = std::nullopt;
// assign a new value:
oEmpty.emplace("Fred");
oEmpty = UserName("Joe");
}
运行结果:
UserName::UserName('Steve')
UserName::~UserName('Steve')
UserName::UserName('Mark')
UserName::~UserName('Mark')
UserName::UserName('Fred')
UserName::UserName('Joe')
UserName::~UserName('Joe')
UserName::~UserName('Joe')
预处理代码如下:
#include
#include
#include
class UserName
{
public:
inline explicit UserName(const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & str)
: mName{std::basic_string<char, std::char_traits<char>, std::allocator<char> >(str)}
{
std::operator<<(std::cout, "UserName::UserName('");
std::operator<<(std::operator<<(std::cout, this->mName), "')\n");
}
inline ~UserName() noexcept
{
std::operator<<(std::cout, "UserName::~UserName('");
std::operator<<(std::operator<<(std::cout, this->mName), "')\n");
}
private:
std::basic_string<char, std::char_traits<char>, std::allocator<char> > mName;
public:
// inline constexpr UserName(const UserName &) noexcept(false) = default;
// inline UserName & operator=(const UserName &) noexcept(false) = default;
};
int main()
{
std::optional<UserName> oEmpty = std::optional<UserName>();
oEmpty.emplace<const char (&)[6]>("Steve");
oEmpty.emplace<const char (&)[5]>("Mark");
oEmpty.reset();
oEmpty.emplace<const char (&)[5]>("Fred");
oEmpty.operator=(UserName(UserName(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("Joe", std::allocator<char>()))));
return 0;
}
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,move
std::string s2 = *os; // OK,拷贝
std::string s3 = std::move(*os); // OK,move
预处理代码如下:
{
std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > os = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >();
std::basic_string<char, std::char_traits<char>, std::allocator<char> > s = std::basic_string<char, std::char_traits<char>, std::allocator<char> >("a very very very long string", std::allocator<char>());
os.operator=(std::move(s));
std::basic_string<char, std::char_traits<char>, std::allocator<char> > s2 = std::basic_string<char, std::char_traits<char>, std::allocator<char> >(os.operator*());
std::basic_string<char, std::char_traits<char>, std::allocator<char> > s3 = std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::move(os.operator*()));
return 0;
}
注意在最后一次调用之后,os
仍然含有一个字符串值,但就像值被移走的对象一样,这个值是未定义的。因此,你可以使用它,但不要对它的值有任何假设。你也可以给它赋一个新的字符串。另外注意有些重载版本会保证临时的可选对象被move
。考虑下面这个返回一个可选字符串的函数:
std::optional<std::string> func();
在这种情况下,下面的代码将会move
临时可选对象的值:
std::string s4 = func().value(); // OK,move
std::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
可选对象的哈希值就等于内含值的哈希值(如果有值的话)。无值的可选对象的哈希值未定义。
一些特定的可选类型可能会导致特殊或意料之外的行为。
将可选对象用作bool
值时使用比较运算符会有特殊的语义。如果内含类型是bool
或者指针类型的话这可能导致令人迷惑的行为。例如:
std::optional<bool> ob{false}; // 值为false
if (!ob) ... // 返回false
if (ob == false) ... // 返回true
std::optional<int*> op{nullptr};
if (!op) ... // 返回false
if (op == nullptr) ... // 返回true
理论上讲,你可以定义可选对象的可选对象:
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<>
将是一个更好的替代。