C++17 引入了 std::variant,它是一个类型安全的联合体,允许在编译时指定一组可能的类型。结合 std::monostate、std::visit 以及 std::optional,我们可以实现一个类似于 Rust 的 Result<T, E> 结构,用来在函数调用中携带成功值或错误信息,而不必依赖异常。下面展示一个完整示例,并解释关键实现细节。
1. 设计 Result
#include <variant>
#include <string>
#include <iostream>
#include <optional>
template <typename T, typename E>
class Result {
public:
// 构造成功结果
static Result ok(T value) { return Result(std::move(value)); }
// 构造错误结果
static Result err(E error) { return Result(std::move(error)); }
// 判断是否成功
bool is_ok() const noexcept { return std::holds_alternative <T>(data_); }
// 获取成功值(若失败则抛异常)
T unwrap() && {
if (!is_ok()) throw std::runtime_error("Called unwrap on Err");
return std::move(std::get <T>(data_));
}
// 获取错误值(若成功则抛异常)
E unwrap_err() && {
if (is_ok()) throw std::runtime_error("Called unwrap_err on Ok");
return std::move(std::get <E>(data_));
}
private:
// 私有构造,强制使用工厂方法
explicit Result(T value) : data_(std::move(value)) {}
explicit Result(E error) : data_(std::move(error)) {}
std::variant<T, E> data_;
};
说明
std::variant<T, E>存储两种可能的状态,编译期保证类型安全。ok与err两个静态工厂方法分别生成成功与错误实例。unwrap与unwrap_err通过std::move返回内部值,避免拷贝。- 对错误情况使用
throw抛出异常;如果需要完全无异常的设计,可返回 `std::optional ` 等。
2. 示例:文件读取
Result<std::string, std::string> read_file(const std::string& path) {
std::ifstream in(path);
if (!in) {
return Result<std::string, std::string>::err("无法打开文件:" + path);
}
std::ostringstream ss;
ss << in.rdbuf();
if (in.fail() && !in.eof()) {
return Result<std::string, std::string>::err("读取文件失败:" + path);
}
return Result<std::string, std::string>::ok(ss.str());
}
3. 调用与错误处理
int main() {
auto res = read_file("example.txt");
if (res.is_ok()) {
std::string content = std::move(res).unwrap();
std::cout << "文件内容:" << content << '\n';
} else {
std::string err_msg = std::move(res).unwrap_err();
std::cerr << "错误:" << err_msg << '\n';
}
return 0;
}
优点
- 类型安全:编译器检查
Ok与Err的返回类型,避免错误的类型转换。 - 无异常:若不想使用异常,完全可以把
unwrap与unwrap_err改为返回std::optional或std::pair<bool, T>。 - 可组合:可以使用
std::visit对Result进行模式匹配,构建链式调用。
4. 进阶:使用 std::expected(C++23 预览)
C++23 标准库已经提供了 std::expected<T, E>,其功能与上述 Result 完全相同。若项目使用 C++23,可直接替换为:
#include <expected>
using Result = std::expected<std::string, std::string>;
5. 小结
std::variant在 C++17 中为实现类似 RustResult的错误处理提供了最直接的工具。- 通过自定义包装类,可以轻松实现成功/错误两种状态,并利用
std::visit进行模式匹配。 - 代码可在不使用异常的情况下实现清晰的错误传播,既保持了性能,又保证了可读性。
将上述模式应用到项目中,可以让错误处理更加明确、可维护,并且与现代 C++ 语言特性高度契合。