Cpp optinal+variant+any+expected 学习笔记
现代C++提供了几个非常实用的类型工具,分别为
std::optional<T>
可能含有T
类型的值或者无值;(C++17引入)std::variant<T1,T2,...>
(类型安全的union)可能含有类型列表中某个类型的值;(正常情况下不会无值)(C++17引入)std::any
可能含有任意类型的值,也可能无值,使用者必须提供自行指定合理的类型;(如果内部的值转换失败会抛异常)(C++17引入)std::expected<T,E>
可能含有正常的T
类型的结果,也可能含有异常的E
类型的值;(C++23引入)
它们的定位和用法比较类似,因此一起整理一下,因为这几个工具的语法仍然在迅速发展中,本文只考虑C++20和C++23已经支持的语法。
需要注意的是,这些类型工具都只能接收简单的类型,不允许使用数组类型和引用类型,并且最好不要含有cv
修饰符。
std::optinal
std::optional<T>
对象的值可能是T
对象,也可能是std::nullopt
(代表没有值),注意这里的T
不允许是数组类型和引用类型。
构造和赋值
在构造时,我们可以提供类型T
作为模板参数(或者根据值来自动推断类型T
),可以提供T
类型的值
1
2
3
4
5std::optional<int> a{10};
std::optional b(10);
auto c = std::optional<int>{10};
std::optional<std::vector<int>> d({1, 2, 3});
不提供参数的默认构造是没有值的 1
2std::optional<int> a;
std::optional<int> b{};
可以使用对应类型的数据进行赋值 1
2std::optional<std::string> a;
a = std::string("hello");
如果对赋值的效率非常敏感,也可以使用emplace()
方法就地构造
1
2std::optional<std::string> a;
a.emplace("hello");
即使已经有值,也可以再次调用emplace()
方法设置新的值。
状态判断
我们可以通过has_value()
方法来判断现在有没有值,也可以将其转换为bool类型,效果是一样的。
1
2
3
4
5
6
7std::optional<int> a;
std::cout << a.has_value() << "\n"; // 0
std::cout << static_cast<bool>(a) << "\n"; // 0
std::optional<int> b(10);
std::cout << b.has_value() << "\n"; // 1
std::cout << static_cast<bool>(b) << "\n"; // 1
需要注意的是,将std:optional<T>
对象转换为bool类型只和它有没有值有关,与T
类型的值是多少没有任何关系。
可以将std::optional
直接用于if
语句中,判断有没有值并进入不同分支
1
2
3
4
5
6
7
8
9
10
11
12std::optional<int> a;
if(a){
// ...
}
else{
// ...
}
if(!a){
// ...
}
获取和重置
我们可以通过value()
方法获取值,在无值时会抛出std::bad_optional_access
异常
1
2
3
4
5std::optional<int> a(10);
std::cout << a.value() << "\n"; // 10
std::optional<int> b{};
std::cout << b.value() << "\n"; // throw error
我们还可以使用value_or(x)
方法以附带默认值的方式安全地获取值,如果无值就会返回默认值
1
2std::optional<int> a;
std::cout << a.value_or(100) << "\n"; // 100
std::optional
拥有类似指针的语义,我们可以通过解引用的方式访问值,包括使用->
访问值的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16std::optional<int> a(10);
std::cout << *a << "\n"; // 10
std::optional<int> b{};
std::cout << *b << "\n"; // UB!
struct Point {
int x;
int y;
};
std::optional<Point> p1 = Point{1, 2};
std::cout << p1->x << " " << p1->y << "\n"; // 1 2
std::optional<Point> p2;
std::cout << p2->x << " " << p2->y << "\n"; // UB!
这种方式比前面的value()
方法效率更高,但是在无值的情况下行为是未定义的:可能直接导致程序在运行期崩溃,也可能返回默认值,或者返回随机值。
使用std::nullopt
赋值,或者调用reset()
方法可以让std::optional
对象变成没有值的状态
1
2
3
4
5
6
7std::optional<int> a(10);
a = std::nullopt;
std::cout << a.has_value() << "\n"; // 0
std::optional<int> b(10);
b.reset();
std::cout << b.has_value() << "\n"; // 0
如果本来就是没有值的状态,这种清理值的行为也不会报错,只是没有什么效果。
使用实例
完整例子如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
std::optional<std::string> func(const std::string &in_str) {
if (in_str.empty()) { return std::nullopt; }
std::string out_str = in_str;
std::reverse(out_str.begin(), out_str.end());
return out_str;
}
void test(const std::string &str) {
std::cout << "{" << str << "}: ";
auto result = func(str);
if (!result) { std::cout << "(Error)" << std::endl; }
else { std::cout << *result << " (Success) " << std::endl; }
}
int main() {
test("");
test("a");
test("abcd");
return 0;
}
运行结果如下 1
2
3{}: (Error)
{a}: a (Success)
{abcd}: dcba (Success)
std::variant
std::variant
就是一个更安全的union,在绝大部分情况下都存储类型列表中的某一个类型的数据,注意类型列表中不允许含有数组类型和引用类型。
在实际操作中都需要自行提供类型参数,并且有两种类型提供方式:第一种是提供类型在类型列表中的索引,第二种是直接提供类型,但是这要求它在类型列表中是唯一的。
构造和赋值
在构造std::variant
时,候选的类型列表是不允许省略的(否则不知道类型是啥了),常见的构造方法如下
1
2
3
4std::variant<char> v0{'a'};
std::variant<int, double> v1 = 10;
std::variant<int, double> v2{2.1};
auto v3 = std::variant<int, double>{1};
可以直接用类型列表中的值对std::variant
对象赋值(或赋值构造),而且赋值可以改变类型
1
2
3
4
5std::variant<int, double> v = 10;
std::cout << v.index() << "\n"; // 0
v = 0.1;
std::cout << v.index() << "\n"; // 1
和std::optional
一样,如果对赋值的效率非常敏感,可以使用emplace<T>()
或emplace<index>()
就地构造
1
2
3std::variant<std::string,int> a;
a.emplace<0>("abc");
a.emplace<std::string>("def");
注意这里需要提供类型参数T
或者类型在类型列表中的索引。
状态判断
可以使用index()
方法获取实际值的类型在类型列表中的索引
1
2
3
4std::variant<int, double, std::string> v = "abc";
std::cout << v.index() << '\n'; // 2
v = 100;
std::cout << v.index() << '\n'; // 0
可以使用std::holds_alternative<T>()
判断当前值是不是属于T
类型,返回布尔值。
1
2
3
4
5std::variant<int, std::string> v = "abc";
std::cout << std::boolalpha << "variant holds int? "
<< std::holds_alternative<int>(v) << '\n'
<< "variant holds string? "
<< std::holds_alternative<std::string>(v) << '\n';
在绝大部分情况下,std::variant
都不会处于无值状态,
但是在某些操作(例如构造,赋值等)中抛出异常确实会导致它处于无值的特殊状态,可以使用valueless_by_exception()
方法检查。
例如在使用之前加上断言,以保证有值 1
assert(var.valueless_by_exception() == false);
获取值
可以使用get<T>()
或get<index>()
方法获取当前的值,需要提供对应类型或对应类型在类型列表中的索引,在非法情况下会直接抛出错误
1
2
3
4std::variant<int, float> v{12};
std::cout << std::get<int>(v) << '\n';
auto w1 = std::get<int>(v);
auto w2 = std::get<0>(v); // same as w1
可以使用
std::get_if<T>()
或std::get_if<index>()
从 std::variant
变量中获取指定类型值的指针:
- 在正常情况下,返回一个指向存储值的指针;
- 在错误情况下(当前持有的不是指定的类型),返回
nullptr
。
例如 1
2
3
4
5
6
7
8
9
10
11std::variant<int, double, std::string> v = 3.14;
if (auto p = std::get_if<int>(&v)) {
std::cout << "Variant own int, value: " << *p << std::endl;
}
else { std::cout << "Variant not own int" << std::endl; }
v = 100;
if (auto p = std::get_if<int>(&v)) {
std::cout << "Variant own int, value: " << *p << std::endl;
}
else { std::cout << "Variant not own int" << std::endl; }
运行结果 1
2Variant not own int
Variant own int, value: 100
可以使用std::visit()
访问std::variant
的值,需要再传递一个可调用对象,例如lambda表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21auto caller = [](auto &&arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "Variant own int, value: " << arg << std::endl;
}
else if constexpr (std::is_same_v<T, double>) {
std::cout << "Variant own double, value: " << arg << std::endl;
}
else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "Variant own std::string, value: " << arg << std::endl;
}
};
std::variant<int, double, std::string> v = 3.14;
std::visit(caller, v);
v = 100;
std::visit(caller, v);
v = "abc";
std::visit(caller, v);
运行结果 1
2
3Variant own double, value: 3.14
Variant own int, value: 100
Variant own std::string, value: abc
基于
std::variant
实现多态也是一种方案。
使用实例
完整例子如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
std::variant<std::string, int, double> func(int s) {
if (s < 0) { return std::string{"Error"}; } // std::string
if (s % 2 == 0) { return s / 2; } // int
return s * 0.5; // double
}
template <typename T>
void test(int s) {
auto v = func(s);
if (auto p = std::get_if<T>(&v)) { std::cout << *p << std::endl; }
}
int main() {
test<int>(10);
test<double>(11);
test<std::string>(-2);
return 0;
}
运行结果如下 1
2
35
5.5
Error
std::any
std::any
可以用于存储任意可以拷贝构造的类型数据,但是用户在读写时需要自行提供类型信息,与实际类型不一致时可能抛出异常。
构造和赋值
构造std::any
的常见方法如下,注意它不是模板类型,我们并不需要提供模板类型参数
1
2
3
4std::any a{std::string{"abc"}};
std::any b(100);
std::any c{3.1};
auto d = std::any{-2};
直接用任何类型的值对std::any
对象赋值(或赋值构造)也是可以的
1
2std::any e = 'c';
e = 100;
如果对赋值的效率非常敏感,可以使用emplace()
方法就地构造。
状态判断
我们可以通过has_value()
方法来判断现在有没有值
1
2
3
4
5std::any a;
std::cout << a.has_value() << "\n"; // 0
std::any b = 1;
std::cout << b.has_value() << "\n"; // 1
但是我们无法获取存储的值的类型信息。
获取和重置
我们需要提供类型将std::any
的值获取出来,通常需要使用std::any_cast<T>()
函数进行转换
1
2
3
4
5
6
7
8
9std::any a{std::string{"abc"}};
std::cout << std::any_cast<std::string>(a) << "\n"; // abc
try {
std::cout << std::any_cast<int>(a) << "\n";
}
catch (const std::bad_any_cast &e) {
std::cout << "bad any cast\n";
}
如果实际类型不一致,可能会抛出异常std::bad_any_cast
。
我们可以通过type()
方法获取std::any
对象的类型信息(字符标记)
1
2
3
4
5
6
7std::any a = 1;
std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
a = 3.14;
std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';
a = true;
std::cout << a.type().name() << ": " << std::boolalpha
<< std::any_cast<bool>(a) << '\n';
输出结果为 1
2
3i: 1
d: 3.14
b: true
这里type()
方法返回的具体结果可能和编译器实现有关。
reset()
方法可以将含有值的std::any
对象重置,此后状态重新变为无值,如果当前是无值的则没有效果
1
2
3
4
5std::any a(10);
std::cout << a.has_value() << "\n"; // 1
a.reset();
std::cout << a.has_value() << "\n"; // 0
使用实例
完整例子如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
std::any func(int s) {
if (s < 0) { return std::string{"Error"}; } // std::string
if (s % 2 == 0) { return s / 2; } // int
return s * 0.5; // double
}
template <typename T>
void test(int s) {
auto v = func(s);
if (auto p = std::any_cast<T>(&v)) { std::cout << *p << std::endl; }
}
int main() {
test<int>(10);
test<double>(11);
test<std::string>(-2);
return 0;
}
运行结果如下 1
2
35
5.5
Error
std::expected
std::expected
是C++参考rust的std::result::Result<T, E>
引入的新工具,虽然在C++23正式推出,但是事实上某些编译器在c++20语法标准下就可以直接使用了。std::expected
被设计用于作为函数的返回值类型,需要包括正常返回值类型T
和错误类型E
:
- 在成功时,存储的是正常情况下的期望值;
- 在失败时,存储的是错误情况下的错误信息。
std::expected
的很多操作和std::optional
非常类似,但是注意没有提供reset()
方法进行重置。
构造和赋值
由于存在两种类型,我们必须在构造时进行显式区分(因为甚至允许这两个类型是一样的,编译器无法作出区分)
- 对于期望值,可以直接传值进行构造;
- 对于错误值,我们必须借助辅助工具类
std::unexpected
进行构造。
1 | std::expected<int, std::string> success_value = 42; |
我们通常在函数的不同分支下,用不同方式构造一个std::expected
对象进行返回,例如
1
2
3
4
5
6std::expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return std::unexpected{"Division by zero"};
}
return a / b;
}
对于无参数的默认构造,会存储期望值类型的默认值 1
std::expected<int, std::string> s; // 0
和构造过程一样,我们可以用期望值或者使用std::unexpected
包装的错误值对std::expected
对象进行重新赋值。
如果对赋值的效率非常敏感,还可以使用emplace()
方法就地构造。
状态判断
可以使用has_value()
方法判断是否含有期望值,也可以将其转换为bool类型,效果是一样的。
1
2
3
4
5
6
7
8std::expected<int, std::string> result;
std::cout << result.has_value() << "\n"; // 1
std::cout << static_cast<bool>(result) << "\n"; // 1
result = std::unexpected{"error"};
std::cout << result.has_value() << "\n"; // 0
std::cout << static_cast<bool>(result) << "\n"; // 0
需要注意的是,将std::expected<T,E>
对象转换为bool类型只和它有没有期望值有关,与T
类型的值是多少没有任何关系。
可以将std::expected<T,E>
直接用于if
语句中,判断有没有值并进入不同分支
1
2
3
4
5
6
7
8
9
10
11
12std::expected<int,std::string> a;
if(a){
// ...
}
else{
// ...
}
if(!a){
// ...
}
获取值
支持如下的方式获取期望值或错误值:
value()
方法,获取期望值,如果不存在,会抛出std::bad_expected_access
异常value_or(x)
方法,获取期望值,如果不存在,就返回期望类型的默认值error()
方法,获取错误值,如果不存在,会抛出std::bad_expected_access
异常error_or(x)
方法,获取错误值,如果不存在,就返回错误类型的默认值
例如 1
2
3
4
5
6
7
8
9
10
11std::expected<int, std::string> a{10};
std::cout << a.value() << "\n"; // 10
std::cout << a.value_or(0) << "\n"; // 10
// std::cout << a.error() << "\n"; // throw std::bad_expected_access
std::cout << a.error_or("No error") << "\n"; // No error
std::expected<int, std::string> b = std::unexpected("Error");
// std::cout << b.value() << "\n"; // throw std::bad_expected_access
std::cout << b.value_or(0) << "\n"; // 0
std::cout << b.error() << "\n"; // Error
std::cout << b.error_or("No error") << "\n"; // Error
std::expected
拥有类似指针的语义,我们可以通过解引用的方式访问值,包括使用->
访问值的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16std::expected<int, std::string> a(10);
std::cout << *a << "\n"; // 10
std::expected<int, std::string> b = std::unexpected("Error");
std::cout << *b << "\n"; // UB!
struct Point {
int x;
int y;
};
std::expected<Point, std::string> p1 = Point{1, 2};
std::cout << p1->x << " " << p1->y << "\n"; // 1 2
std::expected<Point, std::string> p2;
std::cout << p2->x << " " << p2->y << "\n"; // UB!
这种方式比前面的value()
方法效率更高,但是在无期望值的情况下行为是未定义的:可能直接导致程序在运行期崩溃,也可能返回默认值,或者返回随机值。
使用实例
完整例子如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
enum class ErrorType { invalid_input, overflow };
std::expected<int, ErrorType> func(int a) {
if (a == 0) { return std::unexpected{ErrorType::invalid_input}; }
if (a < 0) { return std::unexpected{ErrorType::overflow}; }
return a - 1;
}
int main() {
std::vector<int> data{-1, 0, 1, 2};
for (auto &i : data) {
std::cout << i << ": ";
auto res = func(i);
if (res.has_value()) { std::cout << res.value() << std::endl; }
else {
if (res.error() == ErrorType::invalid_input) {
std::cout << "invalid input" << std::endl;
}
else { std::cout << "overflow" << std::endl; }
}
}
}
运行结果如下 1
2
3
4-1: overflow
0: invalid input
1: 0
2: 1
补充
C++在不断引入函数式编程的新写法,例如这里对std::optional
和std::expected
就支持了很多的算子操作:
std::optional
:and_then(f)
:如果有值,就调用f
并返回结果;否则返回std::optional
默认构造的空对象;or_else(f)
:如果没有值,则保持原样;否则返回f
的调用结果;
std::expected
:and_then(f)
:如果有期望值,就调用f
并返回结果;否则保持原样;or_else(f)
:如果有错误值,就调用f
并返回结果;否则保持原样;
这些操作都需要传递一个回调函数,对于回调函数有很多要求:
- 回调函数必须返回与调用者相同的
std::optional
或std::expected
类型对象; - 大部分情况下,要求回调函数需要一个参数(参数类型是真实的值类型,不是包装后的类型);但是显然
std::optional
的or_else()
要求回调函数没有参数。
使用示例如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
enum class ErrorType { unknown, invalid_input, overflow };
std::expected<int, ErrorType> func(int a) {
if (a == 0) { return std::unexpected{ErrorType::invalid_input}; }
if (a < 0) { return std::unexpected{ErrorType::overflow}; }
return a - 1;
}
int main() {
std::vector<int> data{-1, 0, 1, 2};
for (auto i : data) {
std::cout << i << ": ";
func(i)
.and_then([](int s) {
std::cout << "call add_then\n";
return std::expected<int, ErrorType>{s + 1};
})
.or_else([](ErrorType s) {
std::cout << "call or_else\n";
return std::expected<int, ErrorType>{
std::unexpected{ErrorType::unknown}};
});
}
}
运行结果如下 1
2
3
4-1: call or_else
0: call or_else
1: call add_then
2: call add_then
除此之外,还有一组变换操作:
std::optional
:transform(f)
:如果有值,就调用f
,然后使用返回值来构造std::optional
对象;否则保持原样;
std::expected
:transform(f)
:如果有期望值,就调用f
,然后使用返回的值作为期望值来构造std::expected
对象;否则保持原样;transform_error(f)
:如果有错误值,就调用f
,然后使用返回的值作为错误值来构造std::expected
对象;否则保持原样;
它们和前面一组方法的区别在于:回调函数的任务被简化了,回调函数只需要关注值的处理,返回值类型与输入相同即可,返回值会被再次自动封装为std::optional
或std::expected
类型对象,这个过程不需要回调函数实现。
使用示例如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
enum class ErrorType { unknown, invalid_input, overflow };
std::expected<int, ErrorType> func(int a) {
if (a == 0) { return std::unexpected{ErrorType::invalid_input}; }
if (a < 0) { return std::unexpected{ErrorType::overflow}; }
return a - 1;
}
int main() {
std::vector<int> data{-1, 0, 1, 2};
for (auto i : data) {
std::cout << i << ": ";
func(i)
.transform([](int s) {
std::cout << "call add_then\n";
return s + 1;
})
.transform_error([](ErrorType s) {
std::cout << "call or_else\n";
return ErrorType::unknown;
});
}
}
运行结果同上。