各位 C++ 开发者们,大家好!
在 C++ 的世界里,错误处理一直是一个核心且复杂的话题。从最初的错误码、全局状态,到 C++ 异常机制的引入,我们一直在寻求一种既能清晰表达意图,又能保证性能和代码可维护性的方案。今天,我将向大家隆重介绍 C++23 带来的利器——std::expected,它将为我们提供一种优雅、类型安全且在成功路径上保持 0 运行时开销 的错误处理范式。
传统错误处理的困境与 std::expected 的崛起
在深入探讨 std::expected 之前,让我们快速回顾一下 C++ 中常见的错误处理方式及其局限性。
-
错误码(Error Codes):
- 优点:性能开销极低,尤其是在成功路径上。
- 缺点:
- 容易被忽略:调用者往往会忘记检查返回值。
- 类型不安全:错误码通常是
int或enum,无法携带丰富的错误上下文信息。 - 污染返回值:如果函数本身有返回值,错误码需要通过输出参数、全局变量或结构体来传递,使 API 变得复杂。
- 难以链式调用:多个操作组合时,需要大量的
if语句嵌套。
-
异常(Exceptions):
- 优点:
- 强制处理:未捕获的异常会终止程序,确保错误不会被忽略(至少在顶层)。
- 分离正常逻辑与错误逻辑:使主要业务逻辑更清晰。
- 传播机制:错误可以跨多层函数调用传播。
- 缺点:
- 运行时开销:异常的抛出和捕获涉及栈展开、查找异常处理程序等,性能开销相对较大,尤其是在错误路径上。
- 不可预测性:异常可能在任何地方抛出,使得函数签名无法明确表达其可能抛出的异常类型(尽管有
noexcept,但其语义是“不抛出”而非“可能抛出这些”)。 - 难以局部处理:在底层函数抛出异常,通常意味着要向上层传播,失去了在局部处理错误的机会。
- 与某些场景不兼容:在性能敏感、资源受限或不允许栈展开的场景(如嵌入式系统),异常往往是禁用的。
- 优点:
std::expected 正是为了解决这些痛点而生。它借鉴了函数式编程中 "Result" 或 "Either" 类型的思想,将一个操作可能返回的“成功值”或“错误值”封装在一个单一的、类型安全的容器中。它迫使调用者显式地处理两种可能的结果,同时避免了异常的运行时开销和错误码的类型不安全。
std::expected 的基础:它是什么以及如何使用
std::expected 定义在 <expected> 头文件中,是一个模板类,其基本结构如下:
template<class T, class E>
class expected;
T:表示操作成功时返回的值的类型。E:表示操作失败时返回的错误值的类型。
std::expected<T, E> 的实例要么包含一个 T 类型的值(表示成功),要么包含一个 E 类型的值(表示错误),但不会同时包含两者,也不会同时不包含两者(与 std::optional 的区别在于,std::expected 总是有一个明确的状态:成功或失败)。
构造 std::expected
我们可以通过两种方式构造 std::expected:
-
成功情况:直接使用值构造。
#include <expected> #include <string> #include <iostream> std::expected<int, std::string> Divide(int numerator, int denominator) { if (denominator == 0) { return std::unexpected("Division by zero is not allowed."); // 构造错误值 } return numerator / denominator; // 构造成功值 } int main() { auto result1 = Divide(10, 2); if (result1.has_value()) { std::cout << "10 / 2 = " << result1.value() << std::endl; } else { std::cout << "Error: " << result1.error() << std::endl; } auto result2 = Divide(10, 0); if (result2.has_value()) { std::cout << "10 / 0 = " << result2.value() << std::endl; } else { std::cout << "Error: " << result2.error() << std::endl; } return 0; }在上面的
Divide函数中,我们直接返回numerator / denominator来构造一个成功的std::expected<int, std::string>。 -
错误情况:使用
std::unexpected<E>包装错误值来构造。return std::unexpected("Division by zero is not allowed.");std::unexpected<E>是一个辅助类型,用于明确地标记返回的是一个错误。它确保编译器知道你正在构造一个错误状态的expected。
访问 std::expected 的内容
std::expected 提供了多种方法来安全地访问其内部的值:
has_value()/operator bool():has_value():返回true如果expected包含一个成功值,否则返回false。operator bool():允许std::expected隐式转换为bool,与has_value()效果相同。这是最常用的检查方式。
value():- 如果
expected包含成功值,则返回该值的引用。 - 如果
expected包含错误值,则抛出std::bad_expected_access<E>异常。这类似于std::optional::value()。
- 如果
error():- 如果
expected包含错误值,则返回该值的引用。 - 如果
expected包含成功值,则抛出std::bad_expected_access<E>异常。
- 如果
- *`operator()
和operator->()`**:- 提供对成功值的引用或指针访问,但要求
expected必须包含成功值。在使用前应先通过has_value()或operator bool()进行检查,否则行为未定义。
- 提供对成功值的引用或指针访问,但要求
value_or(const T& default_value):- 如果
expected包含成功值,则返回该值。 - 如果
expected包含错误值,则返回提供的default_value。这是一种便捷的错误恢复方式,适用于可以提供合理默认值的场景。
- 如果
让我们通过一个更复杂的例子来展示这些访问方法:
#include <expected>
#include <string>
#include <iostream>
#include <vector>
#include <fstream>
#include <system_error> // 用于 std::error_code
// 定义一个自定义错误类型
struct FileError {
enum Code {
NotFound,
PermissionDenied,
EmptyFile,
Unknown
} code;
std::string message;
std::string to_string() const {
std::string prefix;
switch (code) {
case NotFound: prefix = "File Not Found"; break;
case PermissionDenied: prefix = "Permission Denied"; break;
case EmptyFile: prefix = "Empty File"; break;
case Unknown: prefix = "Unknown Error"; break;
}
return prefix + ": " + message;
}
};
// 尝试读取文件内容
std::expected<std::string, FileError> ReadFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
return std::unexpected(FileError{FileError::NotFound, "Could not open file: " + filename});
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
if (content.empty()) {
// 通常,如果文件打开了但内容为空,这不是一个“错误”,
// 但为了演示,我们假设空文件是一种错误情况。
return std::unexpected(FileError{FileError::EmptyFile, "File is empty: " + filename});
}
return content;
}
int main() {
// 成功案例
std::string valid_filename = "valid_file.txt";
// 假设 valid_file.txt 存在且有内容
// std::ofstream(valid_filename) << "Hello, expected!"; // 如果没有,先创建
auto file_content1 = ReadFile(valid_filename);
if (file_content1) { // 使用 operator bool()
std::cout << "Content of " << valid_filename << ": " << *file_content1 << std::endl; // 使用 operator*
std::cout << "Value via .value(): " << file_content1.value() << std::endl; // 使用 .value()
} else {
std::cout << "Error reading " << valid_filename << ": " << file_content1.error().to_string() << std::endl;
}
// 失败案例:文件不存在
std::string non_existent_filename = "non_existent.txt";
auto file_content2 = ReadFile(non_existent_filename);
if (!file_content2) {
std::cout << "Error reading " << non_existent_filename << ": " << file_content2.error().to_string() << std::endl;
// 尝试使用 value_or 提供默认值
std::string content_or_default = file_content2.value_or("Default content if file not found.");
std::cout << "Using value_or: " << content_or_default << std::endl;
} else {
std::cout << "Content of " << non_existent_filename << ": " << *file_content2 << std::endl;
}
// 失败案例:空文件 (假设文件存在但内容为空)
std::string empty_filename = "empty_file.txt";
// std::ofstream(empty_filename).close(); // 如果没有,先创建空文件
auto file_content3 = ReadFile(empty_filename);
if (!file_content3) {
std::cout << "Error reading " << empty_filename << ": " << file_content3.error().to_string() << std::endl;
}
// 演示 value() 抛出异常
try {
std::cout << "Attempting to access value() on error expected: " << file_content2.value() << std::endl;
} catch (const std::bad_expected_access<FileError>& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
std::cout << "Error value from exception: " << e.error().to_string() << std::endl;
}
return 0;
}
这个例子展示了如何定义一个自定义错误类型,并在 ReadFile 函数中使用 std::expected 返回结果。在 main 函数中,我们演示了如何通过 operator bool()、operator*()、value()、error() 和 value_or() 来处理成功和失败的情况。
std::expected 的高级用法与链式操作
std::expected 的真正威力在于其提供的成员函数,它们允许我们以函数式风格对结果进行转换和链式操作,极大地简化了错误处理逻辑。
转换与映射:map 和 transform
map(C++23 中引入了 transform 作为 map 的别名)成员函数允许你在 expected 包含成功值时,对其进行转换。如果 expected 包含错误值,则 map 不会执行转换,直接返回原始的错误 expected。
- 签名:
template<class F> auto map(F&& f) &;(也有const&,&&版本) - 行为:如果
*this包含值v,则返回expected<std::invoke_result_t<F, decltype(v)>, E>(std::invoke(std::forward<F>(f), v))。否则,返回*this的副本(包含错误)。
#include <expected>
#include <string>
#include <iostream>
#include <numeric> // For std::accumulate
// 辅助函数:将字符串转换为整数
std::expected<int, std::string> StringToInt(const std::string& s) {
try {
return std::stoi(s);
} catch (const std::exception&) {
return std::unexpected("Failed to convert string '" + s + "' to int.");
}
}
int main() {
auto result1 = StringToInt("123");
auto doubled_result1 = result1.map([](int val) { return val * 2; });
if (doubled_result1) {
std::cout << "Doubled 123: " << *doubled_result1 << std::endl; // 输出 246
} else {
std::cout << "Error: " << doubled_result1.error() << std::endl;
}
auto result2 = StringToInt("abc");
auto doubled_result2 = result2.map([](int val) { return val * 2; });
if (doubled_result2) {
std::cout << "Doubled abc: " << *doubled_result2 << std::endl;
} else {
std::cout << "Error: " << doubled_result2.error() << std::endl; // 输出错误信息
}
// 使用 transform (C++23)
auto transformed_result1 = result1.transform([](int val) { return std::to_string(val); });
if (transformed_result1) {
std::cout << "Transformed 123 to string: " << *transformed_result1 << std::endl; // 输出 "123"
}
return 0;
}
map(或 transform)非常适合于对成功值进行后续处理,而无需关心错误情况,因为错误会自动传播。
扁平化映射:and_then 和 flat_map
and_then(C++23 中引入了 flat_map 作为 and_then 的别名)成员函数用于将一系列可能失败的操作链式连接起来。与 map 不同的是,and_then 的回调函数必须返回另一个 std::expected。如果当前的 expected 包含错误,则 and_then 不会执行回调,直接返回原始的错误 expected。
- 签名:
template<class F> auto and_then(F&& f) &; - 行为:如果
*this包含值v,则返回std::invoke(std::forward<F>(f), v)。否则,返回*this的副本(包含错误)。
这对于构建“管道”式的操作流非常有用,其中每个阶段都可能失败。
#include <expected>
#include <string>
#include <iostream>
#include <vector>
// 1. 从字符串转换为整数
std::expected<int, std::string> ParseInt(const std::string& s) {
try {
return std::stoi(s);
} catch (const std::exception&) {
return std::unexpected("Parsing error: Invalid integer format for '" + s + "'");
}
}
// 2. 检查整数是否在有效范围内
std::expected<int, std::string> ValidateRange(int value) {
if (value >= 0 && value <= 100) {
return value;
} else {
return std::unexpected("Validation error: Value " + std::to_string(value) + " is out of range [0, 100]");
}
}
// 3. 对整数进行特定计算
std::expected<double, std::string> CalculateSquareRoot(int value) {
if (value < 0) {
return std::unexpected("Calculation error: Cannot take square root of negative number " + std::to_string(value));
}
return static_cast<double>(std::sqrt(value));
}
int main() {
std::cout << "--- Successful Chain ---" << std::endl;
auto final_result1 = ParseInt("25")
.and_then([](int val) { return ValidateRange(val); })
.and_then([](int val) { return CalculateSquareRoot(val); })
.map([](double val) { return "Result is: " + std::to_string(val); }); // 最终转换成字符串
if (final_result1) {
std::cout << *final_result1 << std::endl; // Output: Result is: 5.000000
} else {
std::cout << "Error: " << final_result1.error() << std::endl;
}
std::cout << "n--- Failed Chain (ParseInt fails) ---" << std::endl;
auto final_result2 = ParseInt("abc") // 这里会失败
.and_then([](int val) { return ValidateRange(val); })
.and_then([](int val) { return CalculateSquareRoot(val); });
if (final_result2) {
std::cout << "Success: " << *final_result2 << std::endl;
} else {
std::cout << "Error: " << final_result2.error() << std::endl; // Output: Parsing error: Invalid integer format for 'abc'
}
std::cout << "n--- Failed Chain (ValidateRange fails) ---" << std::endl;
auto final_result3 = ParseInt("150")
.and_then([](int val) { return ValidateRange(val); }) // 这里会失败
.and_then([](int val) { return CalculateSquareRoot(val); });
if (final_result3) {
std::cout << "Success: " << *final_result3 << std::endl;
} else {
std::cout << "Error: " << final_result3.error() << std::endl; // Output: Validation error: Value 150 is out of range [0, 100]
}
std::cout << "n--- Failed Chain (CalculateSquareRoot fails) ---" << std::endl;
auto final_result4 = ParseInt("-9")
.and_then([](int val) { return ValidateRange(val); }) // 假设 -9 也在范围内,这里为了演示,只验证了正数开方
.and_then([](int val) { return CalculateSquareRoot(val); }); // 这里会失败
if (final_result4) {
std::cout << "Success: " << *final_result4 << std::endl;
} else {
std::cout << "Error: " << final_result4.error() << std::endl; // Output: Calculation error: Cannot take square root of negative number -9
}
return 0;
}
通过 and_then,我们可以清晰地看到一系列操作的执行流程。一旦任何一个环节返回错误,整个链条就会短路,将错误传播到最终结果,而无需编写冗长的 if (!result) return result; 语句。
错误处理与转换:or_else 和 map_error
-
or_else:当expected包含错误值时,or_else允许你提供一个回调函数来处理错误或尝试恢复。回调函数必须返回一个新的std::expected。如果原始expected包含成功值,则or_else不会执行回调,直接返回原始的成功expected。- 签名:
template<class F> auto or_else(F&& f) &; - 行为:如果
*this包含错误e,则返回std::invoke(std::forward<F>(f), e)。否则,返回*this的副本(包含值)。
- 签名:
-
map_error:当expected包含错误值时,map_error允许你对其进行转换,生成一个新的错误类型。如果原始expected包含成功值,则map_error不会执行转换,直接返回原始的成功expected。- 签名:
template<class F> auto map_error(F&& f) &; - 行为:如果
*this包含错误e,则返回expected<T, std::invoke_result_t<F, decltype(e)>>(std::invoke(std::forward<F>(f), e))。否则,返回*this的副本(包含值)。
- 签名:
#include <expected>
#include <string>
#include <iostream>
#include <system_error> // For std::error_code
enum class AppError {
FileNotFound,
NetworkError,
PermissionDenied,
Unknown
};
// 模拟一个可能失败的函数,返回 AppError
std::expected<std::string, AppError> LoadConfig(const std::string& config_path) {
if (config_path == "missing.conf") {
return std::unexpected(AppError::FileNotFound);
}
if (config_path == "unreadable.conf") {
return std::unexpected(AppError::PermissionDenied);
}
return "Config data from " + config_path; // 成功
}
// 模拟一个尝试从备份加载的函数
std::expected<std::string, AppError> LoadFromBackup() {
std::cout << "Attempting to load from backup..." << std::endl;
// 假设备份加载也可能失败,这里模拟成功
return "Config data from backup.conf";
}
int main() {
std::cout << "--- or_else example ---" << std::endl;
auto config1 = LoadConfig("missing.conf")
.or_else([](AppError err) {
std::cerr << "Primary config failed with error code: " << static_cast<int>(err) << ". Trying backup." << std::endl;
return LoadFromBackup(); // 尝试从备份恢复
});
if (config1) {
std::cout << "Configuration loaded: " << *config1 << std::endl;
} else {
std::cout << "Final error: " << static_cast<int>(config1.error()) << std::endl;
}
std::cout << "n--- map_error example ---" << std::endl;
// 假设我们想把 AppError 转换成更通用的 std::string 错误信息
auto config2 = LoadConfig("unreadable.conf")
.map_error([](AppError err) {
switch (err) {
case AppError::FileNotFound: return "The specified configuration file was not found.";
case AppError::NetworkError: return "Network connection failed while fetching config.";
case AppError::PermissionDenied: return "Access denied to configuration file.";
default: return "An unknown error occurred during configuration loading.";
}
});
if (config2) {
std::cout << "Configuration loaded: " << *config2 << std::endl;
} else {
std::cout << "Transformed error message: " << config2.error() << std::endl;
}
// 演示 map_error 不会影响成功路径
auto config3 = LoadConfig("app.conf")
.map_error([](AppError err) { return "This error message will not appear."; });
if (config3) {
std::cout << "Successfully loaded: " << *config3 << std::endl;
} else {
std::cout << "Error: " << config3.error() << std::endl;
}
return 0;
}
or_else 提供了在错误发生时进行恢复或替换结果的强大机制。map_error 则允许你在不改变成功值类型的情况下,统一或转换错误信息的类型,这对于在不同层次或模块间传递错误非常有用。
std::expected 成员函数速查表
| 成员函数 | 描述 | 当 expected 包含成功值时 |
当 expected 包含错误值时 |
返回类型 |
|---|---|---|---|---|
has_value() |
检查是否包含成功值。 | true |
false |
bool |
operator bool() |
隐式转换为 bool,与 has_value() 相同。 |
true |
false |
bool |
value() |
获取成功值(引用)。 | 返回成功值的引用。 | 抛出 std::bad_expected_access<E> 异常。 |
T& (或 const T&, T&&) |
error() |
获取错误值(引用)。 | 抛出 std::bad_expected_access<E> 异常。 |
返回错误值的引用。 | E& (或 const E&, E&&) |
operator*() |
获取成功值(引用),不检查状态。 | 返回成功值的引用。 | 未定义行为。 | T& (或 const T&, T&&) |
operator->() |
获取成功值(指针),不检查状态。 | 返回指向成功值的指针。 | 未定义行为。 | T* (或 const T*) |
value_or(U&&) |
获取成功值,或在错误时返回默认值。 | 返回成功值的副本。 | 返回 U 类型的副本。 |
T (或 U) |
map(F&&) |
当包含成功值时,对成功值应用函数并返回新的 expected。 |
返回 expected<std::invoke_result_t<F, T>, E>。 |
返回原始错误 expected<T, E> 的副本。 |
expected<NewT, E> |
transform(F&&) |
map 的别名 (C++23)。 |
同 map。 |
同 map。 |
expected<NewT, E> |
and_then(F&&) |
当包含成功值时,对成功值应用函数,且该函数返回一个新的 expected。用于链式调用。 |
返回 std::invoke_result_t<F, T> (必须是 expected<NewT, E>)。 |
返回原始错误 expected<T, E> 的副本。 |
expected<NewT, NewE> (或 expected<NewT, E>) |
flat_map(F&&) |
and_then 的别名 (C++23)。 |
同 and_then。 |
同 and_then。 |
expected<NewT, NewE> (或 expected<NewT, E>) |
or_else(F&&) |
当包含错误值时,对错误值应用函数,且该函数返回一个新的 expected(可用于错误恢复或替换)。 |
返回原始成功 expected<T, E> 的副本。 |
返回 std::invoke_result_t<F, E> (必须是 expected<T, NewE>)。 |
expected<T, NewE> (或 expected<T, E>) |
map_error(F&&) |
当包含错误值时,对错误值应用函数,并返回新的 expected(仅改变错误类型,不改变成功值类型)。 |
返回原始成功 expected<T, E> 的副本。 |
返回 expected<T, std::invoke_result_t<F, E>>。 |
expected<T, NewE> |
std::expected 的 0 运行时开销分析
理解 std::expected 的核心优势之一是其在成功路径上的“0 运行时开销”。这听起来很吸引人,但究竟意味着什么?
内存布局
std::expected<T, E> 的典型实现是基于 union 和一个状态标志位(通常是一个 bool 或 enum)。
template<class T, class E>
class expected {
union Storage {
T value;
E error;
// 构造函数、析构函数等
} m_storage;
bool m_has_value; // 或者一个枚举来表示状态
// ...
};
- 大小:
sizeof(std::expected<T, E>)通常等于max(sizeof(T), sizeof(E))加上一个bool或enum的大小(用于存储状态)。这意味着它不会比单独存储T和E的结构体占用更多内存。 - 无堆内存分配:
std::expected是一个值类型,其内部存储直接在栈上或其所在的对象中,不会引起额外的堆内存分配,这对于性能敏感的应用程序至关重要。
性能考量
-
成功路径上的开销:
- 当
std::expected包含成功值时,对其进行访问(如*result或result.value())的开销与直接访问原始T类型变量的开销几乎相同。 map和and_then等操作在成功路径上,也仅仅是执行了其回调函数,并可能伴随一次移动构造(如果T是非平凡类型)。这与直接调用多个函数并传递中间结果的开销相当。- 最重要的是,它避免了异常处理机制的开销,如栈展开、查找异常处理程序等。对于那些错误极少发生但异常机制开销巨大的场景,
std::expected提供了显著的性能优势。
- 当
-
错误路径上的开销:
- 当
std::expected包含错误值时,访问其错误(如result.error())的开销也与直接访问原始E类型变量相同。 map和and_then在错误路径上会短路,直接返回原始错误,这通常只涉及一个移动或复制操作,开销极低。- 与错误码相比,
std::expected可能会多一个bool成员的检查开销,但这通常是微不足道的,现代 CPU 的分支预测能力可以很好地处理。
- 当
-
分支预测:
if (result.has_value())这样的检查会引入一个条件分支。在大多数应用程序中,成功路径是热路径,错误路径是冷路径。现代 CPU 的分支预测器能够很好地预测这种模式,几乎不会导致性能惩罚。只有在成功和失败路径频繁交替且无法预测时,分支预测失败才可能带来微小的开销。
-
编译期优化:
- 由于
std::expected是一个编译期已知的类型,编译器有机会进行更积极的优化。例如,如果编译器能够确定某个expected总是成功或总是失败,它甚至可以完全消除不必要的条件检查和存储。这使得std::expected在某些情况下比手动编写的错误码处理代码更高效。
- 由于
std::expected 与其他错误处理机制的对比
为了更清晰地理解 std::expected 的定位,我们来做一个简要对比:
| 特性 | 错误码 | 异常 | std::expected |
|---|---|---|---|
| 运行时开销 | 成功路径:极低;错误路径:极低 | 成功路径:几乎为零;错误路径:高 | 成功路径:极低;错误路径:低 |
| 内存开销 | 无额外开销(通过参数或全局变量) | 无额外开销(但栈展开可能消耗内存) | max(sizeof(T), sizeof(E)) + bool |
| 强制处理 | 不强制,容易忽略 | 强制(未捕获会终止) | 强制(需要显式检查 has_value()) |
| 类型安全 | 差(通常是 int,无上下文) |
好(可自定义异常类型,携带丰富信息) | 好(强类型 T 和 E,可携带丰富信息) |
| 错误传播 | 需要手动传递或检查每层返回值 | 自动传播,直到捕获或程序终止 | 需要手动调用 map/and_then 进行链式传播 |
| 链式调用 | 复杂,大量 if 嵌套 |
不支持,需 try-catch 块 |
优雅,通过 map/and_then 实现 |
| 适用场景 | 性能极高要求,或错误信息简单 | 罕见且不可恢复的错误,逻辑与错误分离 | 预期错误,性能敏感,需要丰富错误信息,API 设计 |
何时选择 std::expected?
- 错误是预期的且需要明确处理的:例如,文件不存在、网络连接失败、用户输入无效等。这些错误是业务逻辑的一部分,需要被显式地处理。
- 性能敏感的场景:当你不能承受异常带来的运行时开销时,
std::expected是一个出色的替代方案。 - API 设计:当你的函数可能失败,并且失败的原因需要被调用者清晰地了解和处理时,
std::expected在函数签名中明确表达了这种意图。 - 错误需要携带上下文信息:与简单的错误码不同,
std::expected允许你使用自定义的错误类型E来包含详细的错误信息,如错误码、错误消息、发生位置等。
最佳实践与高级技巧
选择合适的错误类型 E
错误类型 E 的选择至关重要,它直接影响了错误信息的丰富程度和处理的便利性。
-
简单的枚举类型:适用于错误信息非常简单,无需额外上下文的情况。
enum class ParseError { InvalidFormat, OutOfRange, EmptyInput }; std::expected<int, ParseError> parse_number(const std::string& s); -
自定义结构体/类:这是最常见的做法,允许你携带详细的错误上下文。
struct DatabaseError { enum Code { ConnectionFailed, QueryFailed, RecordNotFound } code; std::string message; std::string query_statement; // 额外的上下文信息 }; std::expected<User, DatabaseError> find_user(int user_id); -
std::error_code和std::error_condition:如果你的错误与操作系统或标准库的错误码兼容,可以使用它们。这对于与 C 风格 API 或跨平台错误处理非常有用。#include <system_error> std::expected<std::string, std::error_code> read_file_content(const std::string& path); -
std::variant<E1, E2, ...>:如果一个函数可能产生多种完全不同类型的错误,你可以使用std::variant来封装它们。#include <variant> // 假设有两种不同的错误类型 struct NetworkError { int code; std::string details; }; struct LogicError { std::string reason; }; using MyCombinedError = std::variant<NetworkError, LogicError>; std::expected<Data, MyCombinedError> fetch_and_process_data();处理
std::variant错误时,你需要使用std::visit。
std::expected<void, E>:操作成功但无返回值
有时,一个操作可能成功但不需要返回任何具体的值(例如,一个 void 函数)。在这种情况下,你可以使用 std::expected<void, E>。
- 构造成功:
return {};或return std::expected<void, E>{}; - 构造错误:
return std::unexpected(ErrorType{...});
#include <expected>
#include <string>
#include <iostream>
std::expected<void, std::string> SaveData(const std::string& data, const std::string& filename) {
if (filename.empty()) {
return std::unexpected("Filename cannot be empty.");
}
if (data.empty()) {
// 通常空数据不是错误,但为演示而设
return std::unexpected("Data to save cannot be empty.");
}
// 模拟保存数据
std::cout << "Saving '" << data << "' to " << filename << std::endl;
return {}; // 成功,不返回具体值
}
int main() {
auto result1 = SaveData("Hello World", "my_file.txt");
if (result1) {
std::cout << "Data saved successfully." << std::endl;
} else {
std::cout << "Error: " << result1.error() << std::endl;
}
auto result2 = SaveData("", "another_file.txt");
if (!result2) {
std::cout << "Error: " << result2.error() << std::endl;
}
return 0;
}
与异常的共存与转换
尽管 std::expected 旨在减少异常的使用,但在某些情况下,它们可以并且应该共存。
- 库边界:在库的公共 API 接口处,如果错误是严重的、不可恢复的,或者为了兼容现有代码,可以考虑抛出异常。库内部则可以使用
std::expected。 - 不可恢复的程序错误:内存耗尽、严重的逻辑错误等,这些往往意味着程序状态已损坏,此时抛出异常并终止程序可能是最合理的。
- 异常到
expected转换:在处理可能抛出异常的旧代码或第三方库时,可以使用try-catch块将异常转换为std::expected。std::expected<int, std::string> CallLegacyFunction(const std::string& input) { try { int result = legacy_function(input); // 假设 legacy_function 可能抛出 std::runtime_error return result; } catch (const std::runtime_error& e) { return std::unexpected("Legacy function failed: " + std::string(e.what())); } catch (...) { return std::unexpected("An unknown error occurred in legacy function."); } } expected到异常转换:在应用程序的顶层,如果需要将std::expected的错误转换为异常以便统一处理或终止程序,可以手动抛出。void ProcessTopLevel() { auto result = SomeComplexOperation(); // 返回 std::expected<Data, AppError> if (!result) { throw std::runtime_error("Application critical error: " + result.error().to_string()); } // ... 继续处理 result.value() }
自定义辅助函数
在处理复杂的链式操作或聚合多个 std::expected 结果时,你可能会发现需要一些辅助函数。例如,一个 all 函数,只有当所有 expected 都成功时才返回成功,否则返回第一个遇到的错误。
template <typename T, typename E, typename... Expecteds>
std::expected<std::vector<T>, E> all(std::expected<T, E> first, Expecteds... rest) {
if (!first) {
return std::unexpected(first.error());
}
if constexpr (sizeof...(rest) == 0) {
return std::vector<T>{first.value()};
} else {
auto remaining = all(rest...);
if (!remaining) {
return std::unexpected(remaining.error());
}
std::vector<T> results;
results.push_back(first.value());
results.insert(results.end(), std::make_move_iterator(remaining.value().begin()), std::make_move_iterator(remaining.value().end()));
return results;
}
}
// 示例用法
std::expected<int, std::string> op1() { return 10; }
std::expected<int, std::string> op2() { return std::unexpected("Op2 failed"); }
std::expected<int, std::string> op3() { return 30; }
int main() {
auto all_results1 = all(op1(), op3());
if (all_results1) {
for (int val : *all_results1) {
std::cout << val << " "; // Output: 10 30
}
std::cout << std::endl;
}
auto all_results2 = all(op1(), op2(), op3());
if (!all_results2) {
std::cout << "Error: " << all_results2.error() << std::endl; // Output: Error: Op2 failed
}
return 0;
}
这样的辅助函数可以根据项目的具体需求进行扩展,以适应更复杂的错误聚合逻辑。
std::optional 与 std::expected 的区别
这是一个常见的混淆点。
std::optional<T>:表示“可能存在T类型的值,也可能不存在”。当值不存在时,没有提供原因。std::expected<T, E>:表示“可能存在T类型的值,也可能存在E类型的错误”。当值不存在时,明确告知了原因(通过E)。
简而言之,std::optional 关注“有没有”,而 std::expected 关注“有没有,以及为什么没有”。在需要区分“无值”和“错误”两种状态时,std::expected 是更合适的选择。
实际案例与设计考量
std::expected 适用于任何可能失败且需要明确处理错误的情况。
-
文件操作:
std::expected<std::string, FileError> ReadFile(const std::string& path);std::expected<void, FileError> WriteFile(const std::string& path, const std::string& content);- 错误类型可以包含文件路径、系统错误码等。
-
网络通信:
std::expected<Connection, NetworkError> Connect(const std::string& host, int port);std::expected<HttpResponse, NetworkError> SendRequest(const HttpRequest& request);- 错误类型可以包含网络地址、错误码、超时信息等。
-
解析器:
std::expected<ASTNode, ParseError> Parse(const std::string& input);std::expected<Config, ConfigError> LoadConfig(const std::string& json_string);- 错误类型可以包含行号、列号、错误消息、预期字符等。
-
业务逻辑:
std::expected<Order, OrderProcessingError> ProcessOrder(const OrderRequest& request);std::expected<bool, ValidationError> ValidateUser(const UserData& user);- 错误类型可以包含业务错误码、具体原因、相关实体 ID 等。
API 设计模式:
在设计函数签名时,优先考虑使用 std::expected 作为返回值,而不是 bool 错误码或抛出异常(除非是不可恢复的逻辑错误)。这使得你的 API 意图清晰,调用者可以利用 map 和 and_then 轻松构建健壮的逻辑。
// 不推荐(错误码可能被忽略,且无错误信息)
bool TryOpenDatabase(const std::string& conn_str, Database& db_out);
// 不推荐(可能抛出未知异常,不易追踪和局部处理)
Database OpenDatabase(const std::string& conn_str); // 抛出 DatabaseException
// 推荐(清晰表达成功与失败,并提供错误信息)
std::expected<Database, DatabaseError> OpenDatabase(const std::string& conn_str);
通过这种方式,你的代码库将变得更加健壮、易于理解和维护,同时还能享受到 std::expected 带来的性能优势。
std::expected 是 C++23 中一个令人兴奋的特性,它为现代 C++ 提供了优雅、类型安全且高效的错误处理方案。通过将成功值和错误值封装在同一类型中,它强制调用者处理两种可能性,避免了错误被静默忽略。其丰富的成员函数,如 map、and_then 和 or_else,使得链式操作和错误恢复变得异常简洁。在成功路径上,std::expected 能够实现与直接返回值几乎相同的 0 运行时开销,使其成为性能敏感应用场景的理想选择。拥抱 std::expected,让我们的 C++ 代码更加健壮和富有表现力!