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++ 的重要工具,通过类型安全和显式的错误处理机制,替代了传统的返回码或异常处理方式,提升了代码的可读性和健壮性。