在 C++ 中,std::optionalstd::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):获取值,若为空则返回默认值。
    • *optopt->:通过解引用访问值(需确保有值)。
  • 修改
    • 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):获取值,若为错误则返回默认值。
    • *expexp->:通过解引用访问值(需确保有值)。
  • 修改
    • exp.emplace(args...):在原地构造值。
    • exp = valueexp = 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> 的开销取决于 TE 的大小,通常比 std::optional 稍大(因为需要存储错误类型)。
  • 异常value() 可能抛出异常,建议在确定有值时使用 *exp 或在不确定时使用 value_or
  • 适用场景:适合需要区分成功和失败并传递错误信息的场景,相比 std::optional 更适合复杂的错误处理。

3. std::optional 和 std::expected 的对比

特性std::optionalstd::expected
引入版本C++17C++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_accessvalue()bad_expected_access
存储开销通常为 sizeof(T) + sizeof(bool)取决于 TE 的大小
适用场景查找、初始化、可选配置等文件操作、网络请求、复杂错误处理

选择建议

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