现代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
5
std::optional<int> a{10};
std::optional b(10);
auto c = std::optional<int>{10};

std::optional<std::vector<int>> d({1, 2, 3});

不提供参数的默认构造是没有值的

1
2
std::optional<int> a;
std::optional<int> b{};

可以使用对应类型的数据进行赋值

1
2
std::optional<std::string> a;
a = std::string("hello");

如果对赋值的效率非常敏感,也可以使用emplace()方法就地构造

1
2
std::optional<std::string> a;
a.emplace("hello");

即使已经有值,也可以再次调用emplace()方法设置新的值。

状态判断

我们可以通过has_value()方法来判断现在有没有值,也可以将其转换为bool类型,效果是一样的。

1
2
3
4
5
6
7
std::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
12
std::optional<int> a;

if(a){
// ...
}
else{
// ...
}

if(!a){
// ...
}

获取和重置

我们可以通过value()方法获取值,在无值时会抛出std::bad_optional_access异常

1
2
3
4
5
std::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
2
std::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
16
std::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
7
std::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
#include <algorithm>
#include <iostream>
#include <optional>
#include <string>

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
4
std::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
5
std::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
3
std::variant<std::string,int> a;
a.emplace<0>("abc");
a.emplace<std::string>("def");

注意这里需要提供类型参数T或者类型在类型列表中的索引。

状态判断

可以使用index()方法获取实际值的类型在类型列表中的索引

1
2
3
4
std::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
5
std::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
4
std::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
11
std::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
2
Variant 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
21
auto 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
3
Variant 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
#include <iostream>
#include <variant>
#include <string>

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
3
5
5.5
Error

std::any

std::any可以用于存储任意可以拷贝构造的类型数据,但是用户在读写时需要自行提供类型信息,与实际类型不一致时可能抛出异常。

构造和赋值

构造std::any的常见方法如下,注意它不是模板类型,我们并不需要提供模板类型参数

1
2
3
4
std::any a{std::string{"abc"}};
std::any b(100);
std::any c{3.1};
auto d = std::any{-2};

直接用任何类型的值对std::any对象赋值(或赋值构造)也是可以的

1
2
std::any e = 'c';
e = 100;

如果对赋值的效率非常敏感,可以使用emplace()方法就地构造。

状态判断

我们可以通过has_value()方法来判断现在有没有值

1
2
3
4
5
std::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
9
std::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
7
std::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
3
i: 1
d: 3.14
b: true

这里type()方法返回的具体结果可能和编译器实现有关。

reset()方法可以将含有值的std::any对象重置,此后状态重新变为无值,如果当前是无值的则没有效果

1
2
3
4
5
std::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
#include <any>
#include <iostream>
#include <string>

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
3
5
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
2
std::expected<int, std::string> success_value = 42;
std::expected<int, std::string> error_value = std::unexpected("Some error occurred");

我们通常在函数的不同分支下,用不同方式构造一个std::expected对象进行返回,例如

1
2
3
4
5
6
std::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
8
std::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
12
std::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
11
std::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
16
std::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
#include <expected>
#include <iostream>
#include <vector>

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::optionalstd::expected就支持了很多的算子操作:

  • std::optional
    • and_then(f):如果有值,就调用f并返回结果;否则返回std::optional默认构造的空对象;
    • or_else(f):如果没有值,则保持原样;否则返回f的调用结果;
  • std::expected
    • and_then(f):如果有期望值,就调用f并返回结果;否则保持原样;
    • or_else(f):如果有错误值,就调用f并返回结果;否则保持原样;

这些操作都需要传递一个回调函数,对于回调函数有很多要求:

  • 回调函数必须返回与调用者相同的std::optionalstd::expected类型对象;
  • 大部分情况下,要求回调函数需要一个参数(参数类型是真实的值类型,不是包装后的类型);但是显然std::optionalor_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
#include <expected>
#include <iostream>
#include <vector>

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::optionalstd::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
#include <expected>
#include <iostream>
#include <vector>

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;
});
}
}

运行结果同上。