解读 C++23 std::expected 函数式写法
文章目录
- 一, std::expected 基础概念
- 1.1 什么是 std::expected?
- 1.2 优势
- 与 `std::optional` 和 `std::variant` 的区别
- 二, 函数式写法的功能和应用
- 2.1 `transform` : 对"成功值"进行映射
- 基本用法
- 完全返回不同类型
- 2.2 `and_then` : 对"成功值"进行连续计算
- 2.3 `transform_error` : 对"错误值"进行映射
- 2.4 `or_else` : 对"错误值"进行连续计算
- 小结
- 三, 总结
C++23 带来了一个重要的新功能—std::expected
, 它提供了一种现代化的错误处理方式, 用于表示操作成功的返回值或失败的错误状态. 相比于传统的异常和错误码处理, std::expected
提供了更安全, 更便为, 更类似函数式编程的解决方案. 这篇博客将进一步探索它的基础概念和函数式写法.
一, std::expected 基础概念
1.1 什么是 std::expected?
std::expected
是一个模板类:
template <typename T, typename E>
class std::expected;
T
: 用于表示成功情况下的返回值类型.E
: 用于表示失败情况下的错误值类型.
std::expected
会保存操作的两种状态: 成功或错误. 你可以通过下列方法进行状态检查:
has_value()
: 判断是否包含成功值.error()
: 返回错误值.value()
: 返回成功值, 如果存在错误, 则抱押异常.
以下是一个基础示例:
#include <expected>
#include <iostream>
#include <string>
std::expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return std::unexpected("Division by zero");
}
return a / b;
}
int main() {
if (auto result = divide(10, 2); result) {
std::cout << "Result: " << *result << '\n';
} else {
std::cout << "Error: " << result.error() << '\n';
}
if (auto result = divide(10, 0); result) {
std::cout << "Result: " << *result << '\n';
} else {
std::cout << "Error: " << result.error() << '\n';
}
return 0;
}
1.2 优势
- 明确的错误处理: 强制检查成功或失败状态.
- 类型安全: 避免优化常规问题和异常处理的问题.
- 提高可读性: 代码更为清晰明业.
与 std::optional
和 std::variant
的区别
std::optional
: 仅能表示值的存在或不存在, 无法描述失败的具体原因.
std::variant
: 是多态类型容器, 可以容纳多种类型, 但不限定成功和错误的语义.
std::expected
: 专门用于表示操作成功或失败, 语义明确, 适合函数式错误处理.
二, 函数式写法的功能和应用
2.1 transform
: 对"成功值"进行映射
基本用法
transform
可以将 expected<T, E>
中的成功值转换为另一个类型, 并返回新的 expected<U, E>
.
如果存在错误, 就直接跳过转换, 保留原错误.
示例:
#include <expected>
#include <iostream>
#include <string>
std::expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return std::unexpected("Division by zero");
}
return a / b;
}
void with_transform(int a, int b) {
auto result = divide(a, b).transform([](int value) {
return value * 2; // 成功值 * 2
});
if (result) {
std::cout << "Success: " << *result << '\n'; // Success: 10
} else {
std::cout << "Error: " << result.error() << '\n';
}
}
int main() {
with_transform(10, 2); // 输出: Success: 10
with_transform(10, 0); // 输出: Error: Division by zero
return 0;
}
完全返回不同类型
transform
也支持将成功值转换为全新的类型:
auto process = divide(10, 2)
.transform([](int value) {
return std::to_string(value); // int -> string
})
.transform([](const std::string& str) {
return str.size(); // string -> size_t
});
// 结果为 expected<size_t, std::string>
2.2 and_then
: 对"成功值"进行连续计算
如果需要在成功值上再调用一个返回 std::expected<...>
类型的函数, 可以使用 and_then
.
示例:
std::expected<std::string, std::string> intToString(int x) {
return std::to_string(x);
}
auto result = divide(10, 2)
.and_then([](int value) {
return intToString(value); // 调用另一个返回 expected的函数
});
// result 类型为 expected<std::string, std::string>
如果需要连续计算, 可通过链式调用:
// 可以连续计算多次
auto finalResult =
divide(10, 2)
.and_then([](int value) {
return divide(value, 2); // 再次除法
})
.and_then([](int newValue) -> std::expected<int, std::string> {
if (newValue == 2) {
return std::unexpected("We don't like the value 2!");
}
return newValue * 10;
});
任何一步出现错误, 后续操作都会被跳过.
2.3 transform_error
: 对"错误值"进行映射
transform_error
用于对错误状态中的值进行转换:
auto result = divide(10, 0)
.transform_error([](const std::string& err) {
return "[Transformed Error] " + err;
});
if (!result) {
std::cout << result.error() << std::endl;
// 输出: [Transformed Error] Division by zero
}
2.4 or_else
: 对"错误值"进行连续计算
对错误值进行连续操作, 可以使用 or_else
:
auto handleFileError = [](const std::string& err) {
if (err == "File not found") {
return "Default file content"; // 试图读取默认文件
}
return std::unexpected(err);
};
auto content = openFile("somefile.txt")
.or_else(handleFileError);
if (!content) {
std::cerr << "Error: " << content.error() << std::endl;
} else {
std::cout << "Success: " << *content << std::endl;
}
小结
transform
: 对成功值做"映射" (map), 从 expected<T, E>
得到 expected<U, E>
.
and_then
: 对成功值做"继续计算" (flatMap), 从 expected<T, E>
得到 expected<U, E>
, 而不是嵌套的 expected<expected<...>>
.
transform_error
: 对错误值做"映射", 从 expected<T, E>
得到 expected<T, E2>
.
or_else
: 对错误值做"继续计算", 从 expected<T, E>
得到新的 expected<T, E>
.
这些函数式的组合子让我们在处理多步骤, 且随时可能失败的逻辑时, 代码既能保持简洁, 可读, 又不会丢失错误信息. 任何一步返回错误, 后面的步骤都自动跳过, 错误将直接沿着链路返回给调用端. 这种写法在实际项目中非常有用, 也能减少传统层层 if 检查或异常捕获的繁琐, 使得逻辑更加清晰.
三, 总结
C++23 中的 std::expected
与之配契的函数式结构, 不仅使得代码更为简洁, 还能最大化降低错误处理的应用过过. 通过链式写法, 与成功和失败相关的各种操作可以以一种清晰的方式表达. 日后在处理多步骤, 随时可能失败的计算时, 它将成为你工具箱中不可我缺的一环.