现代C++提供了几个非常实用的值封装类型工具,分别为
它们的定位和用法比较类似,因此一起整理一下,因为这几个工具的语法仍然在迅速发展中,本文只考虑C++20和C++23已经支持的语法。
需要注意的是,这些类型工具都只能接收简单的类型,不允许使用数组类型和引用类型,并且最好不要含有cv修饰符。
std::optional
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"; std::cout << static_cast<bool>(a) << "\n";
std::optional<int> b(10); std::cout << b.has_value() << "\n"; std::cout << static_cast<bool>(b) << "\n";
|
需要注意的是,将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";
std::optional<int> b{}; std::cout << b.value() << "\n";
|
我们还可以使用value_or(x)方法以附带默认值的方式安全地获取值,如果无值就会返回默认值
1 2
| std::optional<int> a; std::cout << a.value_or(100) << "\n";
|
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";
std::optional<int> b{}; std::cout << *b << "\n";
struct Point { int x; int y; };
std::optional<Point> p1 = Point{1, 2}; std::cout << p1->x << " " << p1->y << "\n";
std::optional<Point> p2; std::cout << p2->x << " " << p2->y << "\n";
|
这种方式比前面的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";
std::optional<int> b(10); b.reset(); std::cout << b.has_value() << "\n";
|
如果本来就是没有值的状态,这种清理值的行为也不会报错,只是没有什么效果。
使用实例
完整例子如下
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>
[[nodiscard]] 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)\n"; } else { std::cout << *result << " (Success) \n"; } }
int main() { test(""); test("a"); test("abcd");
return 0; }
|
运行结果如下
1 2 3
| {}: (Error) {a}: a (Success) {abcd}: dcba (Success)
|
实践中建议对返回值为 std::optional 的函数标记为 [[nodiscard]],因为返回值确实是不可忽略的,包含了是否出错的信息。(下面的 std::expected 同样如此)
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";
v = 0.1; std::cout << v.index() << "\n";
|
和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'; v = 100; std::cout << v.index() << '\n';
|
可以使用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);
|
可以使用 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 << '\n'; } else { std::cout << "Variant not own int\n"; }
v = 100; if (auto p = std::get_if<int>(&v)) { std::cout << "Variant own int, value: " << *p << '\n'; } else { std::cout << "Variant not own int\n"; }
|
运行结果
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 << '\n'; } else if constexpr (std::is_same_v<T, double>) { std::cout << "Variant own double, value: " << arg << '\n'; } else if constexpr (std::is_same_v<T, std::string>) { std::cout << "Variant own std::string, value: " << arg << '\n'; } };
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"}; }
if (s % 2 == 0) { return s / 2; }
return s * 0.5; }
template <typename T> void test(int s) { auto v = func(s); if (auto p = std::get_if<T>(&v)) { std::cout << *p << '\n'; } }
int main() { test<int>(10); test<double>(11); test<std::string>(-2);
return 0; }
|
运行结果如下
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";
std::any b = 1; std::cout << b.has_value() << "\n";
|
但是我们无法获取存储的值的类型信息。
获取和重置
我们需要提供类型将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";
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';
|
输出结果为
这里type()方法返回的具体结果可能和编译器实现有关。
reset()方法可以将含有值的std::any对象重置,此后状态重新变为无值,如果当前是无值的则没有效果
1 2 3 4 5
| std::any a(10); std::cout << a.has_value() << "\n";
a.reset(); std::cout << a.has_value() << "\n";
|
使用实例
完整例子如下
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"}; }
if (s % 2 == 0) { return s / 2; }
return s * 0.5; }
template <typename T> void test(int s) { auto v = func(s); if (auto p = std::any_cast<T>(&v)) { std::cout << *p << '\n'; } }
int main() { test<int>(10); test<double>(11); test<std::string>(-2);
return 0; }
|
运行结果如下
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;
|
和构造过程一样,我们可以用期望值或者使用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"; std::cout << static_cast<bool>(result) << "\n";
result = std::unexpected{"error"}; std::cout << result.has_value() << "\n"; std::cout << static_cast<bool>(result) << "\n";
|
需要注意的是,将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"; std::cout << a.value_or(0) << "\n";
std::cout << a.error_or("No error") << "\n";
std::expected<int, std::string> b = std::unexpected("Error");
std::cout << b.value_or(0) << "\n"; std::cout << b.error() << "\n"; std::cout << b.error_or("No error") << "\n";
|
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";
std::expected<int, std::string> b = std::unexpected("Error"); std::cout << *b << "\n";
struct Point { int x; int y; };
std::expected<Point, std::string> p1 = Point{1, 2}; std::cout << p1->x << " " << p1->y << "\n";
std::expected<Point, std::string> p2; std::cout << p2->x << " " << p2->y << "\n";
|
这种方式比前面的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 };
[[nodiscard]] 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() << '\n'; } else { if (res.error() == ErrorType::invalid_input) { std::cout << "invalid input\n"; } else { std::cout << "overflow\n"; } } } }
|
运行结果如下
1 2 3 4
| -1: overflow 0: invalid input 1: 0 2: 1
|
极简实现
目前还是有很多环境下无法使用std::expected,下面提供一份极简的实现,基于std::variant封装一下,模拟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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #pragma once
#include <stdexcept> #include <type_traits> #include <utility> #include <variant>
namespace mini {
template <typename E> class unexpected { public: explicit unexpected(const E &err) : m_error(err) {}
explicit unexpected(E &&err) : m_error(std::move(err)) {}
const E &value() const & { return m_error; }
E &value() & { return m_error; }
E &&value() && { return std::move(m_error); }
private: E m_error; };
template <typename T, typename E> class expected { static_assert(!std::is_same_v<T, unexpected<E>>, "T must not be unexpected<E>");
public: expected(const T &value) : m_data(value), m_valid(true) {}
expected(T &&value) : m_data(std::move(value)), m_valid(true) {}
expected(unexpected<E> err) : m_data(std::move(err)), m_valid(false) {}
bool has_value() const noexcept { return m_valid; }
explicit operator bool() const noexcept { return m_valid; }
T &value() { if (!m_valid) throw std::runtime_error("Bad expected access: no value"); return std::get<T>(m_data); }
const T &value() const { if (!m_valid) throw std::runtime_error("Bad expected access: no value"); return std::get<T>(m_data); }
E &error() { if (m_valid) throw std::runtime_error("Bad expected access: no error"); return std::get<unexpected<E>>(m_data).value(); }
const E &error() const { if (m_valid) throw std::runtime_error("Bad expected access: no error"); return std::get<unexpected<E>>(m_data).value(); }
private: std::variant<T, unexpected<E>> m_data; bool m_valid; };
}
|
补充
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
| #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::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
| #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; }); } }
|
运行结果同上。