std::expected
C++23 引入的一个模板类,用于显式表示可能成功返回一个值或失败返回一个错误的操作。它结合了类型安全和错误处理的优点,是传统错误码(error code)和异常(exceptions)的替代方案。
错误处理的逻辑关系为条件关系,若正确,则执行A 逻辑;若失败,则执行B 逻辑,并需要知
道确切的错误信息,才能对症下药。当前的常用方式是通过错误码或异常,但使用起来还是多有不
便。
std::expected<T, E> 表示期望,算是std::variant 和std::optional 的结合,它要么保留T(期望的类型),要么保留E(错误的类型),它的接口又和std::optional 相似。
核心概念
-
设计目标:显式表示可能失败的操作,避免隐式错误处理(如异常)或全局错误码的弊端。
-
两种状态:
-
预期值(Expected Value):操作成功时存储的返回值。
-
错误(Error):操作失败时存储的错误信息。
-
-
类型安全:错误和成功值的类型在编译时确定,避免运行时类型错误。
std::expected
在标头 | ||
template< class T, class E > | (1) | (C++23 起) |
template< class T, class E > requires std::is_void_v<T> class expected<T, E>; | (2) | (C++23 起) |
类模板 std::expected
提供表示两个值之一的方式:它要么表示一个 T
类型的预期 值,要么表示一个 E
类型的非预期 值。expected
决不会无值。
1) 主模板。在自身的存储中包含预期值或非预期值,该值内嵌于 expected
对象。
2) void 部分特化。表示一个 void 类型的预期值或在自身的存储中包含非预期值。如果包含非预期值,那么该值内嵌于 expected
对象。
如果程序以引用类型、函数类型,或 std::unexpected 的特化实例化 expected
,那么程序非良构。另外,T
必须不是 std::in_place_t 或 std::unexpect_t。
模板形参
T | - | 预期值的类型。类型必须是(可有 cv 限定的)void,或者符合可析构 (Destructible) 要求(尤其是不允许数组或引用类型)。 |
E | - | 非预期值的类型。类型必须符合可析构 (Destructible) 要求,且必须对于 std::unexpected 为合法的模板实参(尤其是不允许数组、非对象类型及 cv 限定的类型)。 |
成员类型
成员类型 | 定义 |
value_type | T |
error_type | E |
unexpected_type | std::unexpected<E> |
成员别名模板
类型 | 定义 |
rebind<U> | std::expected<U, error_type> |
数据成员
成员 | 定义 |
bool has_val | expected 对象当前是否表示预期值(仅用于阐述的成员对象*) |
T val (仅限主模板) | 预期值 (仅用于阐述的变体成员对象*) |
E unex | 非预期值 (仅用于阐述的变体成员对象*) |
成员函数
(构造函数) | 构造 expected 对象(公开成员函数) |
(析构函数) | 销毁 expected 对象以及其所含的值(公开成员函数) |
operator= | 赋值内容 (公开成员函数) |
观察器 | |
operator->operator* | 访问预期值 (公开成员函数) |
operator boolhas_value | 检查对象是否含有预期值 (公开成员函数) |
value | 返回预期值 (公开成员函数) |
error | 返回非预期值 (公开成员函数) |
value_or | 如果有预期值则返回它,否则返回另一个值 (公开成员函数) |
error_or | 如果有非预期值则返回它,否则返回另一个值 (公开成员函数) |
单子操作 | |
and_then | 若存在预期值则返回给定的函数在其上的结果,否则返回 expected 本身(公开成员函数) |
transform | 若存在预期值则返回含有变换后的预期值的 expected ,否则返回 expected 本身(公开成员函数) |
or_else | 若 expected 含有预期值则返回其自身,否则返回给定的函数在非预期值上的结果(公开成员函数) |
transform_error | 若含有预期值则返回 expected 本身,否则返回含有变换后非预期值的 expected (公开成员函数) |
修改器 | |
emplace | 原位构造预期值 (公开成员函数) |
swap | 交换内容 (公开成员函数) |
非成员函数
operator== (C++23) | 比较 expected 对象(函数模板) |
swap(std::expected) (C++23) | 特化 std::swap 算法 (函数) |
辅助类
unexpected (C++23) | 表示一个非预期值 (类模板) |
bad_expected_access (C++23) | 指示对含有非预期值的 expected 的有检查访问的异常(类模板) |
unexpect_tunexpect (C++23) | expected 中非预期值的原位构造标签(类) (常量) |
基本用法
1. 定义与初始化
#include <expected>
#include <string>// 定义一个可能返回 int 或字符串错误的 expected 类型
std::expected<int, std::string> parse_number(const std::string& input) {try {return std::stoi(input);} catch (...) {return std::unexpected("Invalid number format");}
}
2. 检查状态
auto result = parse_number("123");
if (result.has_value())
{std::cout << "Value: " << *result << std::endl;
}
else
{std::cout << "Error: " << result.error() << std::endl;
}
3. 访问值或错误
-
直接访问
int value = result.value(); // 成功时返回值,失败时抛出 std::bad_expected_access
std::string err = result.error();// 失败时返回错误
安全访问:
int value = result.value_or(0); // 失败时返回默认值 0
4. transform
:转换成功值
- 功能:若
std::expected
包含成功值,则对其应用一个函数,返回新的std::expected
;若包含错误,则直接传递错误。 - 适用场景:对成功值进行纯转换(不涉及可能失败的操作)。
示例:将整数结果转换为字符串
#include <expected>
#include <string>
#include <iostream>std::expected<int, std::string> computeValue(bool success)
{if (success){return 42;}return std::unexpected("Error");
}int main()
{auto result = computeValue(true).transform([](int x) { return "Answer: " + std::to_string(x*2); });if (result) {std::cout << *result << "\n"; // 输出: Answer: 42} else {std::cout << result.error() << "\n";}
}
5. and_then
:链式执行可能失败的操作
- 功能:若
std::expected
包含成功值,则对其应用一个返回新std::expected返回值类型
的函数;若包含错误,直接传递错误。 - 适用场景:需要连续执行多个可能失败的操作(例如:先读取文件,再解析内容)。
示例:链式除法操作
#include <expected>
#include <string>
#include <iostream>std::expected<double, std::string> safeDivide(double a, double b)
{if (b != 0){return a / b;}return std::unexpected("Division by zero");
}int main()
{auto result = safeDivide(10, 2).and_then([](double x) { return safeDivide(x, 5); }) // 10/2=5 → 5/5=1.and_then([](double x) { return safeDivide(x, 0); }); // 1/0 → 错误if (result) {std::cout << *result << "\n";} else {std::cout << "Error: " << result.error() << "\n"; // 输出: Error: Division by zero}
}
6. or_else
:处理错误
- 功能:若
std::expected
包含错误,则对其应用一个处理函数(返回新std::expected
);若包含成功值,直接传递成功值。 - 适用场景:错误恢复、日志记录或提供默认值。
示例:错误恢复和日志记录
#include <expected>
#include <string>
#include <iostream>std::expected<int, std::string> readConfig() {return std::unexpected("Config file missing");
}int main() {auto result = readConfig().or_else([](const auto& error) {std::cerr << "Log: " << error << "\n"; // 记录错误日志return std::expected<int, std::string>(100); // 提供默认值});std::cout << "Final value: " << *result << "\n"; // 输出: Final value: 100
}
一个简单的例子:
enum class Status : uint8_t
{Ok,connection_error,no_authority,format_error,
};bool connected()
{return true;
}bool has_authority()
{return false;
}bool format()
{return false;
}std::expected<std::string, Status> read_data()
{if (!connected())return std::unexpected<Status> { Status::connection_error };if (!has_authority())return std::unexpected<Status> { Status::no_authority };if (!format())return std::unexpected<Status> { Status::format_error };return {"my expected type"};
}int main()
{auto result = read_data();if (result) {std::cout << result.value() << "\n";} else {std::cout << "error code: " << (int)result.error() << "\n";}
}
这种方式无疑会简化错误处理的操作。
示例场景
1. 文件读取
#include <fstream>
#include <vector>std::expected<std::vector<char>, std::string> read_file(const std::string& path)
{std::ifstream file(path, std::ios::binary);if (!file) {return std::unexpected("Failed to open file");}std::vector<char> data;file.seekg(0, std::ios::end);data.resize(file.tellg());file.seekg(0, std::ios::beg);file.read(data.data(), data.size());if (file.fail()) {return std::unexpected("Failed to read file");}return data;
}// 使用示例
auto data = read_file("config.txt");
if (data)
{process_data(*data);
}
else
{log_error(data.error());
}
2. 数学计算
std::expected<double, std::string> safe_divide(double a, double b)
{if (b == 0) {return std::unexpected("Division by zero");}return a / b;
}// 使用示例
auto result = safe_divide(10, 2);
if (result)
{std::cout << "Result: " << *result << std::endl;
}
Monadic 操作(C++23)
std::expected
支持链式操作,类似函数式编程中的 map
和 and_then
:
#include <iostream>
#include <type_traits>
#include <utility> // for std::forward_like
#include <expected>std::expected<int, std::string> validate(int x) {if (x < 0) return std::unexpected("Negative value");return x;
}std::expected<int, std::string> process(int x) {return x * 2;
}int main()
{// 链式调用auto result = validate(42).and_then(process) // 仅在成功时调用 process.transform([](int x) { return x + 1; }); // 转换值if (result) {std::cout << "Final value: " << *result << std::endl; // 输出 85}
}
对比其他错误处理方式
方式 | 优点 | 缺点 |
---|---|---|
异常 | 自动传播错误 | 性能开销,控制流不透明 |
错误码 | 无性能开销 | 易被忽略,类型不安全 |
std::expected | 显式错误,类型安全,性能高效 | 需要手动处理错误状态 |
注意事项
-
错误类型:错误类型可以是任意类型(如
std::error_code
、自定义枚举或字符串)。 -
性能:
std::expected
无动态内存分配,适合性能敏感场景。 -
与异常结合:可以包装可能抛出异常的操作(如示例中的
parse_number
)。 -
编译器支持:需要支持 C++23 的编译器(如 GCC 13、Clang 16+)。
总结
std::expected
提供了一种类型安全、显式的错误处理机制,适用于替代传统错误码或异常的场景。它通过模板参数明确区分成功值和错误类型,结合 Monadic 操作可以编写更清晰的链式逻辑。对于需要高性能和可预测性的代码(如库开发或嵌入式系统),std::expected
是一个强大的工具。