c++ optional & expected
在 C++ 中,std::optional 和 std::expected 是两个用于处理可能不存在的值或可能失败的操作的现代工具,它们在 C++17 和 C++23 中分别引入,提供了更安全和表达力更强的错误处理机制。以下是对它们的详细说明,包括定义、用途、特性、用法、差异以及实际代码示例。
1. std::optional
定义
std::optional 是 C++17 引入的标准库组件,定义在 <optional> 头文件中。它表示一个可能存在或不存在的值,类似于“可能有值,也可能为空”的概念。std::optional<T> 可以包含类型为 T 的值,或者为空(std::nullopt)。
用途
- 用于表示函数可能返回一个值,也可能不返回任何值。
- 替代传统的“特殊值”(如
nullptr、-1等)来表示无值的情况,增强代码的可读性和类型安全性。 - 常用于需要明确区分“无值”和“有值”的场景,如查找操作、初始化状态等。
关键特性
- 类型安全:
std::optional明确表示值是否有效,避免了隐式的“魔法值”问题。 - 值语义:
std::optional<T>存储T的值或空状态,值存储在optional对象内部(通常通过 placement new 实现)。 - 不抛出异常:访问
std::optional的值时,如果值不存在,行为是明确定义的(例如,通过value()抛出std::bad_optional_access)。 - 轻量:
std::optional通常只比T多占用一个布尔标志的存储空间(用于标记是否有值)。
主要接口
- 构造:
std::optional<T> opt;:构造空的optional。std::optional<T> opt(value);:构造包含值的optional。std::optional<T> opt(std::nullopt);:构造空的optional。
- 访问:
opt.has_value():检查是否包含值。opt.value():获取值,若为空则抛出std::bad_optional_access。opt.value_or(default_value):获取值,若为空则返回默认值。*opt和opt->:通过解引用访问值(需确保有值)。
- 修改:
opt.emplace(args...):在原地构造值。opt = value:赋值。opt.reset():清空值。
- 比较:
- 支持
==,!=,<,>,<=,>=,空值被认为是最小的。
- 支持
代码示例
#include <iostream>
#include <optional>
#include <string>
std::optional<std::string> find_name(int id) {
if (id == 1) {
return "Alice";
}
return std::nullopt; // 没有找到
}
int main() {
auto result = find_name(1);
if (result.has_value()) {
std::cout << "Found: " << *result << '\n';
} else {
std::cout << "Not found\n";
}
// 使用 value_or 提供默认值
std::cout << find_name(2).value_or("Unknown") << '\n';
// 使用 value()(需小心,可能抛异常)
try {
std::cout << find_name(2).value() << '\n';
} catch (const std::bad_optional_access& e) {
std::cout << "Error: " << e.what() << '\n';
}
std::cout << "__GNUC__:" << __GNUC__ << std::endl;
std::cout << "__GNUC_MINOR__:" << __GNUC_MINOR__ << std::endl;
}注意事项
- 性能:
std::optional的开销通常很小,但如果T是一个大对象,可能需要考虑移动语义或emplace构造以避免拷贝。 - 异常:
value()可能抛出异常,建议在确定有值时使用*opt或在不确定时使用value_or。 - 适用场景:适合表示“值可能不存在”的情况,但不适合表示“操作可能失败并需要错误信息”的场景。
2. std::expected
定义
std::expected 是 C++23 引入的标准库组件,定义在 <expected> 头文件中。它表示一个操作的结果,要么是期望的值(类型为 T),要么是错误(类型为 E)。它可以看作是 std::optional 的增强版,增加了对错误信息的支持。
用途
- 用于表示可能成功或失败的操作,返回值要么是成功的值,要么是失败的错误信息。
- 替代传统的错误处理方式(如返回码、异常等),提供更结构化的错误处理。
- 常用于需要传递错误原因的场景,如文件操作、网络请求等。
关键特性
- 双值模型:
std::expected<T, E>存储一个T类型的值(成功情况)或一个E类型的错误(失败情况)。 - 类型安全:通过类型系统区分成功和失败状态,避免了隐式错误处理。
- 不抛出异常(默认):访问值时,如果结果是错误状态,行为明确(通过
error()获取错误)。 - 灵活的错误类型:
E可以是任何类型(如枚举、字符串、自定义错误类),提供了丰富的错误表达能力。
主要接口
- 构造:
std::expected<T, E> exp(value);:构造包含值的expected。std::expected<T, E> exp(std::unexpect, error);:构造包含错误的expected。
- 访问:
exp.has_value():检查是否包含值。exp.value():获取值,若为错误则抛出std::bad_expected_access<E>。exp.error():获取错误,若为值则行为未定义(需先检查has_value())。exp.value_or(default_value):获取值,若为错误则返回默认值。*exp和exp->:通过解引用访问值(需确保有值)。
- 修改:
exp.emplace(args...):在原地构造值。exp = value或exp = std::unexpect, error:赋值。
- 链式操作:
and_then():如果有值,执行一个返回expected的函数。or_else():如果有错误,执行一个返回expected的函数。transform():转换值。transform_error():转换错误。
代码示例
#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() {
auto result = divide(10, 2);
if (result.has_value()) {
std::cout << "Result: " << *result << '\n';
} else {
std::cout << "Error: " << result.error() << '\n';
}
auto result2 = divide(10, 0);
if (!result2) {
std::cout << "Error: " << result2.error() << '\n';
}
// 使用 value_or
std::cout << divide(10, 0).value_or(-1) << '\n';
// 链式操作
auto chained = divide(10, 2)
.and_then([](int x) { return std::expected<int, std::string>(x * 2); })
.or_else([](const std::string& err) {
return std::expected<int, std::string>(std::unexpected("Handled: " + err));
});
std::cout << "Chained: " << *chained << '\n';
}输出:
Result: 5
Error: Division by zero
-1
Chained: 10注意事项
- 性能:
std::expected<T, E>的开销取决于T和E的大小,通常比std::optional稍大(因为需要存储错误类型)。 - 异常:
value()可能抛出异常,建议在确定有值时使用*exp或在不确定时使用value_or。 - 适用场景:适合需要区分成功和失败并传递错误信息的场景,相比
std::optional更适合复杂的错误处理。
3. std::optional 和 std::expected 的对比
| 特性 | std::optional | std::expected |
|---|---|---|
| 引入版本 | C++17 | C++23 |
| 头文件 | <optional> | <expected> |
| 表示内容 | 值(T)或无值(std::nullopt) | 值(T)或错误(E) |
| 错误处理 | 无错误信息,仅表示值不存在 | 支持错误信息(E 可自定义) |
| 主要用途 | 表示可能不存在的值 | 表示可能失败的操作及其错误原因 |
| 访问接口 | has_value(), value(), value_or() | has_value(), value(), error(), value_or() |
| 链式操作 | 无 | 支持 and_then(), or_else(), transform() 等 |
| 异常 | value() 抛 bad_optional_access | value() 抛 bad_expected_access |
| 存储开销 | 通常为 sizeof(T) + sizeof(bool) | 取决于 T 和 E 的大小 |
| 适用场景 | 查找、初始化、可选配置等 | 文件操作、网络请求、复杂错误处理 |
选择建议
- 使用
std::optional:- 当只需要表示“值是否存在”而不需要额外错误信息时。
- 例如:查找操作(如
find()返回一个可能为空的结果)、可选参数。
- 使用
std::expected:- 当需要处理可能失败的操作并携带错误信息时。
- 例如:文件读写、网络请求、解析输入等需要明确错误原因的场景。
4. 实际应用场景
std::optional
#include <optional>
#include <string>
#include <map>
#include <iostream>
std::optional<std::string> get_user_name(const std::map<int, std::string>& users, int id) {
auto it = users.find(id);
if (it != users.end()) {
return it->second;
}
return std::nullopt;
}
int main() {
std::map<int, std::string> users = {{1, "Alice"}, {2, "Bob"}};
auto name = get_user_name(users, 1);
std::cout << (name ? *name : "Not found") << '\n'; // 输出: Alice
name = get_user_name(users, 3);
std::cout << (name ? *name : "Not found") << '\n'; // 输出: Not found
}std::expected
#include <expected>
#include <string>
#include <iostream>
std::expected<std::string, std::string> read_file(const std::string& filename) {
if (filename.empty()) {
return std::unexpected("Empty filename");
}
// 假设文件读取逻辑
return "File content";
}
int main() {
auto result = read_file("example.txt");
if (result) {
std::cout << "Content: " << *result << '\n';
} else {
std::cout << "Error: " << result.error() << '\n';
}
result = read_file("");
if (!result) {
std::cout << "Error: " << result.error() << '\n'; // 输出: Error: Empty filename
}
}5. 总结
std::optional适合简单的“值或无值”场景,接口简洁,适用于查找、可选参数等场景。std::expected更适合需要错误处理的场景,提供了更强大的错误传递和链式操作能力,适用于复杂操作。- 两者都是现代 C++ 的重要工具,通过类型安全和显式的错误处理机制,替代了传统的返回码或异常处理方式,提升了代码的可读性和健壮性。







