如何利用 C++23 中的 `std::expected` 优雅处理错误并保持 0 运行时开销?

各位 C++ 开发者们,大家好!

在 C++ 的世界里,错误处理一直是一个核心且复杂的话题。从最初的错误码、全局状态,到 C++ 异常机制的引入,我们一直在寻求一种既能清晰表达意图,又能保证性能和代码可维护性的方案。今天,我将向大家隆重介绍 C++23 带来的利器——std::expected,它将为我们提供一种优雅、类型安全且在成功路径上保持 0 运行时开销 的错误处理范式。

传统错误处理的困境与 std::expected 的崛起

在深入探讨 std::expected 之前,让我们快速回顾一下 C++ 中常见的错误处理方式及其局限性。

  1. 错误码(Error Codes)

    • 优点:性能开销极低,尤其是在成功路径上。
    • 缺点
      • 容易被忽略:调用者往往会忘记检查返回值。
      • 类型不安全:错误码通常是 intenum,无法携带丰富的错误上下文信息。
      • 污染返回值:如果函数本身有返回值,错误码需要通过输出参数、全局变量或结构体来传递,使 API 变得复杂。
      • 难以链式调用:多个操作组合时,需要大量的 if 语句嵌套。
  2. 异常(Exceptions)

    • 优点
      • 强制处理:未捕获的异常会终止程序,确保错误不会被忽略(至少在顶层)。
      • 分离正常逻辑与错误逻辑:使主要业务逻辑更清晰。
      • 传播机制:错误可以跨多层函数调用传播。
    • 缺点
      • 运行时开销:异常的抛出和捕获涉及栈展开、查找异常处理程序等,性能开销相对较大,尤其是在错误路径上。
      • 不可预测性:异常可能在任何地方抛出,使得函数签名无法明确表达其可能抛出的异常类型(尽管有 noexcept,但其语义是“不抛出”而非“可能抛出这些”)。
      • 难以局部处理:在底层函数抛出异常,通常意味着要向上层传播,失去了在局部处理错误的机会。
      • 与某些场景不兼容:在性能敏感、资源受限或不允许栈展开的场景(如嵌入式系统),异常往往是禁用的。

std::expected 正是为了解决这些痛点而生。它借鉴了函数式编程中 "Result" 或 "Either" 类型的思想,将一个操作可能返回的“成功值”或“错误值”封装在一个单一的、类型安全的容器中。它迫使调用者显式地处理两种可能的结果,同时避免了异常的运行时开销和错误码的类型不安全。

std::expected 的基础:它是什么以及如何使用

std::expected 定义在 <expected> 头文件中,是一个模板类,其基本结构如下:

template<class T, class E>
class expected;
  • T:表示操作成功时返回的值的类型。
  • E:表示操作失败时返回的错误值的类型。

std::expected<T, E> 的实例要么包含一个 T 类型的值(表示成功),要么包含一个 E 类型的值(表示错误),但不会同时包含两者,也不会同时不包含两者(与 std::optional 的区别在于,std::expected 总是有一个明确的状态:成功或失败)。

构造 std::expected

我们可以通过两种方式构造 std::expected

  1. 成功情况:直接使用值构造。

    #include <expected>
    #include <string>
    #include <iostream>
    
    std::expected<int, std::string> Divide(int numerator, int denominator) {
        if (denominator == 0) {
            return std::unexpected("Division by zero is not allowed."); // 构造错误值
        }
        return numerator / denominator; // 构造成功值
    }
    
    int main() {
        auto result1 = Divide(10, 2);
        if (result1.has_value()) {
            std::cout << "10 / 2 = " << result1.value() << std::endl;
        } else {
            std::cout << "Error: " << result1.error() << std::endl;
        }
    
        auto result2 = Divide(10, 0);
        if (result2.has_value()) {
            std::cout << "10 / 0 = " << result2.value() << std::endl;
        } else {
            std::cout << "Error: " << result2.error() << std::endl;
        }
        return 0;
    }

    在上面的 Divide 函数中,我们直接返回 numerator / denominator 来构造一个成功的 std::expected<int, std::string>

  2. 错误情况:使用 std::unexpected<E> 包装错误值来构造。

    return std::unexpected("Division by zero is not allowed.");

    std::unexpected<E> 是一个辅助类型,用于明确地标记返回的是一个错误。它确保编译器知道你正在构造一个错误状态的 expected

访问 std::expected 的内容

std::expected 提供了多种方法来安全地访问其内部的值:

  • has_value() / operator bool()
    • has_value():返回 true 如果 expected 包含一个成功值,否则返回 false
    • operator bool():允许 std::expected 隐式转换为 bool,与 has_value() 效果相同。这是最常用的检查方式。
  • value()
    • 如果 expected 包含成功值,则返回该值的引用。
    • 如果 expected 包含错误值,则抛出 std::bad_expected_access<E> 异常。这类似于 std::optional::value()
  • error()
    • 如果 expected 包含错误值,则返回该值的引用。
    • 如果 expected 包含成功值,则抛出 std::bad_expected_access<E> 异常。
  • *`operator()operator->()`**:
    • 提供对成功值的引用或指针访问,但要求 expected 必须包含成功值。在使用前应先通过 has_value()operator bool() 进行检查,否则行为未定义
  • value_or(const T& default_value)
    • 如果 expected 包含成功值,则返回该值。
    • 如果 expected 包含错误值,则返回提供的 default_value。这是一种便捷的错误恢复方式,适用于可以提供合理默认值的场景。

让我们通过一个更复杂的例子来展示这些访问方法:

#include <expected>
#include <string>
#include <iostream>
#include <vector>
#include <fstream>
#include <system_error> // 用于 std::error_code

// 定义一个自定义错误类型
struct FileError {
    enum Code {
        NotFound,
        PermissionDenied,
        EmptyFile,
        Unknown
    } code;
    std::string message;

    std::string to_string() const {
        std::string prefix;
        switch (code) {
            case NotFound: prefix = "File Not Found"; break;
            case PermissionDenied: prefix = "Permission Denied"; break;
            case EmptyFile: prefix = "Empty File"; break;
            case Unknown: prefix = "Unknown Error"; break;
        }
        return prefix + ": " + message;
    }
};

// 尝试读取文件内容
std::expected<std::string, FileError> ReadFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        return std::unexpected(FileError{FileError::NotFound, "Could not open file: " + filename});
    }

    std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    if (content.empty()) {
        // 通常,如果文件打开了但内容为空,这不是一个“错误”,
        // 但为了演示,我们假设空文件是一种错误情况。
        return std::unexpected(FileError{FileError::EmptyFile, "File is empty: " + filename});
    }
    return content;
}

int main() {
    // 成功案例
    std::string valid_filename = "valid_file.txt";
    // 假设 valid_file.txt 存在且有内容
    // std::ofstream(valid_filename) << "Hello, expected!"; // 如果没有,先创建

    auto file_content1 = ReadFile(valid_filename);
    if (file_content1) { // 使用 operator bool()
        std::cout << "Content of " << valid_filename << ": " << *file_content1 << std::endl; // 使用 operator*
        std::cout << "Value via .value(): " << file_content1.value() << std::endl; // 使用 .value()
    } else {
        std::cout << "Error reading " << valid_filename << ": " << file_content1.error().to_string() << std::endl;
    }

    // 失败案例:文件不存在
    std::string non_existent_filename = "non_existent.txt";
    auto file_content2 = ReadFile(non_existent_filename);
    if (!file_content2) {
        std::cout << "Error reading " << non_existent_filename << ": " << file_content2.error().to_string() << std::endl;
        // 尝试使用 value_or 提供默认值
        std::string content_or_default = file_content2.value_or("Default content if file not found.");
        std::cout << "Using value_or: " << content_or_default << std::endl;
    } else {
        std::cout << "Content of " << non_existent_filename << ": " << *file_content2 << std::endl;
    }

    // 失败案例:空文件 (假设文件存在但内容为空)
    std::string empty_filename = "empty_file.txt";
    // std::ofstream(empty_filename).close(); // 如果没有,先创建空文件

    auto file_content3 = ReadFile(empty_filename);
    if (!file_content3) {
        std::cout << "Error reading " << empty_filename << ": " << file_content3.error().to_string() << std::endl;
    }

    // 演示 value() 抛出异常
    try {
        std::cout << "Attempting to access value() on error expected: " << file_content2.value() << std::endl;
    } catch (const std::bad_expected_access<FileError>& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
        std::cout << "Error value from exception: " << e.error().to_string() << std::endl;
    }

    return 0;
}

这个例子展示了如何定义一个自定义错误类型,并在 ReadFile 函数中使用 std::expected 返回结果。在 main 函数中,我们演示了如何通过 operator bool()operator*()value()error()value_or() 来处理成功和失败的情况。

std::expected 的高级用法与链式操作

std::expected 的真正威力在于其提供的成员函数,它们允许我们以函数式风格对结果进行转换和链式操作,极大地简化了错误处理逻辑。

转换与映射:maptransform

map(C++23 中引入了 transform 作为 map 的别名)成员函数允许你在 expected 包含成功值时,对其进行转换。如果 expected 包含错误值,则 map 不会执行转换,直接返回原始的错误 expected

  • 签名template<class F> auto map(F&& f) &; (也有 const&, && 版本)
  • 行为:如果 *this 包含值 v,则返回 expected<std::invoke_result_t<F, decltype(v)>, E>(std::invoke(std::forward<F>(f), v))。否则,返回 *this 的副本(包含错误)。
#include <expected>
#include <string>
#include <iostream>
#include <numeric> // For std::accumulate

// 辅助函数:将字符串转换为整数
std::expected<int, std::string> StringToInt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (const std::exception&) {
        return std::unexpected("Failed to convert string '" + s + "' to int.");
    }
}

int main() {
    auto result1 = StringToInt("123");
    auto doubled_result1 = result1.map([](int val) { return val * 2; });

    if (doubled_result1) {
        std::cout << "Doubled 123: " << *doubled_result1 << std::endl; // 输出 246
    } else {
        std::cout << "Error: " << doubled_result1.error() << std::endl;
    }

    auto result2 = StringToInt("abc");
    auto doubled_result2 = result2.map([](int val) { return val * 2; });

    if (doubled_result2) {
        std::cout << "Doubled abc: " << *doubled_result2 << std::endl;
    } else {
        std::cout << "Error: " << doubled_result2.error() << std::endl; // 输出错误信息
    }

    // 使用 transform (C++23)
    auto transformed_result1 = result1.transform([](int val) { return std::to_string(val); });
    if (transformed_result1) {
        std::cout << "Transformed 123 to string: " << *transformed_result1 << std::endl; // 输出 "123"
    }

    return 0;
}

map(或 transform)非常适合于对成功值进行后续处理,而无需关心错误情况,因为错误会自动传播。

扁平化映射:and_thenflat_map

and_then(C++23 中引入了 flat_map 作为 and_then 的别名)成员函数用于将一系列可能失败的操作链式连接起来。与 map 不同的是,and_then 的回调函数必须返回另一个 std::expected。如果当前的 expected 包含错误,则 and_then 不会执行回调,直接返回原始的错误 expected

  • 签名template<class F> auto and_then(F&& f) &;
  • 行为:如果 *this 包含值 v,则返回 std::invoke(std::forward<F>(f), v)。否则,返回 *this 的副本(包含错误)。

这对于构建“管道”式的操作流非常有用,其中每个阶段都可能失败。

#include <expected>
#include <string>
#include <iostream>
#include <vector>

// 1. 从字符串转换为整数
std::expected<int, std::string> ParseInt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (const std::exception&) {
        return std::unexpected("Parsing error: Invalid integer format for '" + s + "'");
    }
}

// 2. 检查整数是否在有效范围内
std::expected<int, std::string> ValidateRange(int value) {
    if (value >= 0 && value <= 100) {
        return value;
    } else {
        return std::unexpected("Validation error: Value " + std::to_string(value) + " is out of range [0, 100]");
    }
}

// 3. 对整数进行特定计算
std::expected<double, std::string> CalculateSquareRoot(int value) {
    if (value < 0) {
        return std::unexpected("Calculation error: Cannot take square root of negative number " + std::to_string(value));
    }
    return static_cast<double>(std::sqrt(value));
}

int main() {
    std::cout << "--- Successful Chain ---" << std::endl;
    auto final_result1 = ParseInt("25")
        .and_then([](int val) { return ValidateRange(val); })
        .and_then([](int val) { return CalculateSquareRoot(val); })
        .map([](double val) { return "Result is: " + std::to_string(val); }); // 最终转换成字符串

    if (final_result1) {
        std::cout << *final_result1 << std::endl; // Output: Result is: 5.000000
    } else {
        std::cout << "Error: " << final_result1.error() << std::endl;
    }

    std::cout << "n--- Failed Chain (ParseInt fails) ---" << std::endl;
    auto final_result2 = ParseInt("abc") // 这里会失败
        .and_then([](int val) { return ValidateRange(val); })
        .and_then([](int val) { return CalculateSquareRoot(val); });

    if (final_result2) {
        std::cout << "Success: " << *final_result2 << std::endl;
    } else {
        std::cout << "Error: " << final_result2.error() << std::endl; // Output: Parsing error: Invalid integer format for 'abc'
    }

    std::cout << "n--- Failed Chain (ValidateRange fails) ---" << std::endl;
    auto final_result3 = ParseInt("150")
        .and_then([](int val) { return ValidateRange(val); }) // 这里会失败
        .and_then([](int val) { return CalculateSquareRoot(val); });

    if (final_result3) {
        std::cout << "Success: " << *final_result3 << std::endl;
    } else {
        std::cout << "Error: " << final_result3.error() << std::endl; // Output: Validation error: Value 150 is out of range [0, 100]
    }

    std::cout << "n--- Failed Chain (CalculateSquareRoot fails) ---" << std::endl;
    auto final_result4 = ParseInt("-9")
        .and_then([](int val) { return ValidateRange(val); }) // 假设 -9 也在范围内,这里为了演示,只验证了正数开方
        .and_then([](int val) { return CalculateSquareRoot(val); }); // 这里会失败

    if (final_result4) {
        std::cout << "Success: " << *final_result4 << std::endl;
    } else {
        std::cout << "Error: " << final_result4.error() << std::endl; // Output: Calculation error: Cannot take square root of negative number -9
    }
    return 0;
}

通过 and_then,我们可以清晰地看到一系列操作的执行流程。一旦任何一个环节返回错误,整个链条就会短路,将错误传播到最终结果,而无需编写冗长的 if (!result) return result; 语句。

错误处理与转换:or_elsemap_error

  • or_else:当 expected 包含错误值时,or_else 允许你提供一个回调函数来处理错误或尝试恢复。回调函数必须返回一个新的 std::expected。如果原始 expected 包含成功值,则 or_else 不会执行回调,直接返回原始的成功 expected

    • 签名template<class F> auto or_else(F&& f) &;
    • 行为:如果 *this 包含错误 e,则返回 std::invoke(std::forward<F>(f), e)。否则,返回 *this 的副本(包含值)。
  • map_error:当 expected 包含错误值时,map_error 允许你对其进行转换,生成一个新的错误类型。如果原始 expected 包含成功值,则 map_error 不会执行转换,直接返回原始的成功 expected

    • 签名template<class F> auto map_error(F&& f) &;
    • 行为:如果 *this 包含错误 e,则返回 expected<T, std::invoke_result_t<F, decltype(e)>>(std::invoke(std::forward<F>(f), e))。否则,返回 *this 的副本(包含值)。
#include <expected>
#include <string>
#include <iostream>
#include <system_error> // For std::error_code

enum class AppError {
    FileNotFound,
    NetworkError,
    PermissionDenied,
    Unknown
};

// 模拟一个可能失败的函数,返回 AppError
std::expected<std::string, AppError> LoadConfig(const std::string& config_path) {
    if (config_path == "missing.conf") {
        return std::unexpected(AppError::FileNotFound);
    }
    if (config_path == "unreadable.conf") {
        return std::unexpected(AppError::PermissionDenied);
    }
    return "Config data from " + config_path; // 成功
}

// 模拟一个尝试从备份加载的函数
std::expected<std::string, AppError> LoadFromBackup() {
    std::cout << "Attempting to load from backup..." << std::endl;
    // 假设备份加载也可能失败,这里模拟成功
    return "Config data from backup.conf";
}

int main() {
    std::cout << "--- or_else example ---" << std::endl;
    auto config1 = LoadConfig("missing.conf")
        .or_else([](AppError err) {
            std::cerr << "Primary config failed with error code: " << static_cast<int>(err) << ". Trying backup." << std::endl;
            return LoadFromBackup(); // 尝试从备份恢复
        });

    if (config1) {
        std::cout << "Configuration loaded: " << *config1 << std::endl;
    } else {
        std::cout << "Final error: " << static_cast<int>(config1.error()) << std::endl;
    }

    std::cout << "n--- map_error example ---" << std::endl;
    // 假设我们想把 AppError 转换成更通用的 std::string 错误信息
    auto config2 = LoadConfig("unreadable.conf")
        .map_error([](AppError err) {
            switch (err) {
                case AppError::FileNotFound: return "The specified configuration file was not found.";
                case AppError::NetworkError: return "Network connection failed while fetching config.";
                case AppError::PermissionDenied: return "Access denied to configuration file.";
                default: return "An unknown error occurred during configuration loading.";
            }
        });

    if (config2) {
        std::cout << "Configuration loaded: " << *config2 << std::endl;
    } else {
        std::cout << "Transformed error message: " << config2.error() << std::endl;
    }

    // 演示 map_error 不会影响成功路径
    auto config3 = LoadConfig("app.conf")
        .map_error([](AppError err) { return "This error message will not appear."; });
    if (config3) {
        std::cout << "Successfully loaded: " << *config3 << std::endl;
    } else {
        std::cout << "Error: " << config3.error() << std::endl;
    }

    return 0;
}

or_else 提供了在错误发生时进行恢复或替换结果的强大机制。map_error 则允许你在不改变成功值类型的情况下,统一或转换错误信息的类型,这对于在不同层次或模块间传递错误非常有用。

std::expected 成员函数速查表

成员函数 描述 expected 包含成功值时 expected 包含错误值时 返回类型
has_value() 检查是否包含成功值。 true false bool
operator bool() 隐式转换为 bool,与 has_value() 相同。 true false bool
value() 获取成功值(引用)。 返回成功值的引用。 抛出 std::bad_expected_access<E> 异常。 T& (或 const T&, T&&)
error() 获取错误值(引用)。 抛出 std::bad_expected_access<E> 异常。 返回错误值的引用。 E& (或 const E&, E&&)
operator*() 获取成功值(引用),不检查状态。 返回成功值的引用。 未定义行为 T& (或 const T&, T&&)
operator->() 获取成功值(指针),不检查状态。 返回指向成功值的指针。 未定义行为 T* (或 const T*)
value_or(U&&) 获取成功值,或在错误时返回默认值。 返回成功值的副本。 返回 U 类型的副本。 T (或 U)
map(F&&) 当包含成功值时,对成功值应用函数并返回新的 expected 返回 expected<std::invoke_result_t<F, T>, E> 返回原始错误 expected<T, E> 的副本。 expected<NewT, E>
transform(F&&) map 的别名 (C++23)。 map map expected<NewT, E>
and_then(F&&) 当包含成功值时,对成功值应用函数,且该函数返回一个新的 expected。用于链式调用。 返回 std::invoke_result_t<F, T> (必须是 expected<NewT, E>)。 返回原始错误 expected<T, E> 的副本。 expected<NewT, NewE> (或 expected<NewT, E>)
flat_map(F&&) and_then 的别名 (C++23)。 and_then and_then expected<NewT, NewE> (或 expected<NewT, E>)
or_else(F&&) 当包含错误值时,对错误值应用函数,且该函数返回一个新的 expected(可用于错误恢复或替换)。 返回原始成功 expected<T, E> 的副本。 返回 std::invoke_result_t<F, E> (必须是 expected<T, NewE>)。 expected<T, NewE> (或 expected<T, E>)
map_error(F&&) 当包含错误值时,对错误值应用函数,并返回新的 expected(仅改变错误类型,不改变成功值类型)。 返回原始成功 expected<T, E> 的副本。 返回 expected<T, std::invoke_result_t<F, E>> expected<T, NewE>

std::expected 的 0 运行时开销分析

理解 std::expected 的核心优势之一是其在成功路径上的“0 运行时开销”。这听起来很吸引人,但究竟意味着什么?

内存布局

std::expected<T, E> 的典型实现是基于 union 和一个状态标志位(通常是一个 boolenum)。

template<class T, class E>
class expected {
    union Storage {
        T value;
        E error;
        // 构造函数、析构函数等
    } m_storage;
    bool m_has_value; // 或者一个枚举来表示状态
    // ...
};
  • 大小sizeof(std::expected<T, E>) 通常等于 max(sizeof(T), sizeof(E)) 加上一个 boolenum 的大小(用于存储状态)。这意味着它不会比单独存储 TE 的结构体占用更多内存。
  • 无堆内存分配std::expected 是一个值类型,其内部存储直接在栈上或其所在的对象中,不会引起额外的堆内存分配,这对于性能敏感的应用程序至关重要。

性能考量

  1. 成功路径上的开销

    • std::expected 包含成功值时,对其进行访问(如 *resultresult.value())的开销与直接访问原始 T 类型变量的开销几乎相同。
    • mapand_then 等操作在成功路径上,也仅仅是执行了其回调函数,并可能伴随一次移动构造(如果 T 是非平凡类型)。这与直接调用多个函数并传递中间结果的开销相当。
    • 最重要的是,它避免了异常处理机制的开销,如栈展开、查找异常处理程序等。对于那些错误极少发生但异常机制开销巨大的场景,std::expected 提供了显著的性能优势。
  2. 错误路径上的开销

    • std::expected 包含错误值时,访问其错误(如 result.error())的开销也与直接访问原始 E 类型变量相同。
    • mapand_then 在错误路径上会短路,直接返回原始错误,这通常只涉及一个移动或复制操作,开销极低。
    • 与错误码相比,std::expected 可能会多一个 bool 成员的检查开销,但这通常是微不足道的,现代 CPU 的分支预测能力可以很好地处理。
  3. 分支预测

    • if (result.has_value()) 这样的检查会引入一个条件分支。在大多数应用程序中,成功路径是热路径,错误路径是冷路径。现代 CPU 的分支预测器能够很好地预测这种模式,几乎不会导致性能惩罚。只有在成功和失败路径频繁交替且无法预测时,分支预测失败才可能带来微小的开销。
  4. 编译期优化

    • 由于 std::expected 是一个编译期已知的类型,编译器有机会进行更积极的优化。例如,如果编译器能够确定某个 expected 总是成功或总是失败,它甚至可以完全消除不必要的条件检查和存储。这使得 std::expected 在某些情况下比手动编写的错误码处理代码更高效。

std::expected 与其他错误处理机制的对比

为了更清晰地理解 std::expected 的定位,我们来做一个简要对比:

特性 错误码 异常 std::expected
运行时开销 成功路径:极低;错误路径:极低 成功路径:几乎为零;错误路径:高 成功路径:极低;错误路径:低
内存开销 无额外开销(通过参数或全局变量) 无额外开销(但栈展开可能消耗内存) max(sizeof(T), sizeof(E)) + bool
强制处理 不强制,容易忽略 强制(未捕获会终止) 强制(需要显式检查 has_value()
类型安全 差(通常是 int,无上下文) 好(可自定义异常类型,携带丰富信息) 好(强类型 TE,可携带丰富信息)
错误传播 需要手动传递或检查每层返回值 自动传播,直到捕获或程序终止 需要手动调用 map/and_then 进行链式传播
链式调用 复杂,大量 if 嵌套 不支持,需 try-catch 优雅,通过 map/and_then 实现
适用场景 性能极高要求,或错误信息简单 罕见且不可恢复的错误,逻辑与错误分离 预期错误,性能敏感,需要丰富错误信息,API 设计

何时选择 std::expected

  • 错误是预期的且需要明确处理的:例如,文件不存在、网络连接失败、用户输入无效等。这些错误是业务逻辑的一部分,需要被显式地处理。
  • 性能敏感的场景:当你不能承受异常带来的运行时开销时,std::expected 是一个出色的替代方案。
  • API 设计:当你的函数可能失败,并且失败的原因需要被调用者清晰地了解和处理时,std::expected 在函数签名中明确表达了这种意图。
  • 错误需要携带上下文信息:与简单的错误码不同,std::expected 允许你使用自定义的错误类型 E 来包含详细的错误信息,如错误码、错误消息、发生位置等。

最佳实践与高级技巧

选择合适的错误类型 E

错误类型 E 的选择至关重要,它直接影响了错误信息的丰富程度和处理的便利性。

  1. 简单的枚举类型:适用于错误信息非常简单,无需额外上下文的情况。

    enum class ParseError {
        InvalidFormat,
        OutOfRange,
        EmptyInput
    };
    std::expected<int, ParseError> parse_number(const std::string& s);
  2. 自定义结构体/类:这是最常见的做法,允许你携带详细的错误上下文。

    struct DatabaseError {
        enum Code {
            ConnectionFailed,
            QueryFailed,
            RecordNotFound
        } code;
        std::string message;
        std::string query_statement; // 额外的上下文信息
    };
    std::expected<User, DatabaseError> find_user(int user_id);
  3. std::error_codestd::error_condition:如果你的错误与操作系统或标准库的错误码兼容,可以使用它们。这对于与 C 风格 API 或跨平台错误处理非常有用。

    #include <system_error>
    std::expected<std::string, std::error_code> read_file_content(const std::string& path);
  4. std::variant<E1, E2, ...>:如果一个函数可能产生多种完全不同类型的错误,你可以使用 std::variant 来封装它们。

    #include <variant>
    // 假设有两种不同的错误类型
    struct NetworkError { int code; std::string details; };
    struct LogicError { std::string reason; };
    
    using MyCombinedError = std::variant<NetworkError, LogicError>;
    std::expected<Data, MyCombinedError> fetch_and_process_data();

    处理 std::variant 错误时,你需要使用 std::visit

std::expected<void, E>:操作成功但无返回值

有时,一个操作可能成功但不需要返回任何具体的值(例如,一个 void 函数)。在这种情况下,你可以使用 std::expected<void, E>

  • 构造成功return {};return std::expected<void, E>{};
  • 构造错误return std::unexpected(ErrorType{...});
#include <expected>
#include <string>
#include <iostream>

std::expected<void, std::string> SaveData(const std::string& data, const std::string& filename) {
    if (filename.empty()) {
        return std::unexpected("Filename cannot be empty.");
    }
    if (data.empty()) {
        // 通常空数据不是错误,但为演示而设
        return std::unexpected("Data to save cannot be empty.");
    }
    // 模拟保存数据
    std::cout << "Saving '" << data << "' to " << filename << std::endl;
    return {}; // 成功,不返回具体值
}

int main() {
    auto result1 = SaveData("Hello World", "my_file.txt");
    if (result1) {
        std::cout << "Data saved successfully." << std::endl;
    } else {
        std::cout << "Error: " << result1.error() << std::endl;
    }

    auto result2 = SaveData("", "another_file.txt");
    if (!result2) {
        std::cout << "Error: " << result2.error() << std::endl;
    }
    return 0;
}

与异常的共存与转换

尽管 std::expected 旨在减少异常的使用,但在某些情况下,它们可以并且应该共存。

  • 库边界:在库的公共 API 接口处,如果错误是严重的、不可恢复的,或者为了兼容现有代码,可以考虑抛出异常。库内部则可以使用 std::expected
  • 不可恢复的程序错误:内存耗尽、严重的逻辑错误等,这些往往意味着程序状态已损坏,此时抛出异常并终止程序可能是最合理的。
  • 异常到 expected 转换:在处理可能抛出异常的旧代码或第三方库时,可以使用 try-catch 块将异常转换为 std::expected
    std::expected<int, std::string> CallLegacyFunction(const std::string& input) {
        try {
            int result = legacy_function(input); // 假设 legacy_function 可能抛出 std::runtime_error
            return result;
        } catch (const std::runtime_error& e) {
            return std::unexpected("Legacy function failed: " + std::string(e.what()));
        } catch (...) {
            return std::unexpected("An unknown error occurred in legacy function.");
        }
    }
  • expected 到异常转换:在应用程序的顶层,如果需要将 std::expected 的错误转换为异常以便统一处理或终止程序,可以手动抛出。
    void ProcessTopLevel() {
        auto result = SomeComplexOperation(); // 返回 std::expected<Data, AppError>
        if (!result) {
            throw std::runtime_error("Application critical error: " + result.error().to_string());
        }
        // ... 继续处理 result.value()
    }

自定义辅助函数

在处理复杂的链式操作或聚合多个 std::expected 结果时,你可能会发现需要一些辅助函数。例如,一个 all 函数,只有当所有 expected 都成功时才返回成功,否则返回第一个遇到的错误。

template <typename T, typename E, typename... Expecteds>
std::expected<std::vector<T>, E> all(std::expected<T, E> first, Expecteds... rest) {
    if (!first) {
        return std::unexpected(first.error());
    }
    if constexpr (sizeof...(rest) == 0) {
        return std::vector<T>{first.value()};
    } else {
        auto remaining = all(rest...);
        if (!remaining) {
            return std::unexpected(remaining.error());
        }
        std::vector<T> results;
        results.push_back(first.value());
        results.insert(results.end(), std::make_move_iterator(remaining.value().begin()), std::make_move_iterator(remaining.value().end()));
        return results;
    }
}

// 示例用法
std::expected<int, std::string> op1() { return 10; }
std::expected<int, std::string> op2() { return std::unexpected("Op2 failed"); }
std::expected<int, std::string> op3() { return 30; }

int main() {
    auto all_results1 = all(op1(), op3());
    if (all_results1) {
        for (int val : *all_results1) {
            std::cout << val << " "; // Output: 10 30
        }
        std::cout << std::endl;
    }

    auto all_results2 = all(op1(), op2(), op3());
    if (!all_results2) {
        std::cout << "Error: " << all_results2.error() << std::endl; // Output: Error: Op2 failed
    }
    return 0;
}

这样的辅助函数可以根据项目的具体需求进行扩展,以适应更复杂的错误聚合逻辑。

std::optionalstd::expected 的区别

这是一个常见的混淆点。

  • std::optional<T>:表示“可能存在 T 类型的值,也可能不存在”。当值不存在时,没有提供原因。
  • std::expected<T, E>:表示“可能存在 T 类型的值,也可能存在 E 类型的错误”。当值不存在时,明确告知了原因(通过 E)。

简而言之,std::optional 关注“有没有”,而 std::expected 关注“有没有,以及为什么没有”。在需要区分“无值”和“错误”两种状态时,std::expected 是更合适的选择。

实际案例与设计考量

std::expected 适用于任何可能失败且需要明确处理错误的情况。

  1. 文件操作

    • std::expected<std::string, FileError> ReadFile(const std::string& path);
    • std::expected<void, FileError> WriteFile(const std::string& path, const std::string& content);
    • 错误类型可以包含文件路径、系统错误码等。
  2. 网络通信

    • std::expected<Connection, NetworkError> Connect(const std::string& host, int port);
    • std::expected<HttpResponse, NetworkError> SendRequest(const HttpRequest& request);
    • 错误类型可以包含网络地址、错误码、超时信息等。
  3. 解析器

    • std::expected<ASTNode, ParseError> Parse(const std::string& input);
    • std::expected<Config, ConfigError> LoadConfig(const std::string& json_string);
    • 错误类型可以包含行号、列号、错误消息、预期字符等。
  4. 业务逻辑

    • std::expected<Order, OrderProcessingError> ProcessOrder(const OrderRequest& request);
    • std::expected<bool, ValidationError> ValidateUser(const UserData& user);
    • 错误类型可以包含业务错误码、具体原因、相关实体 ID 等。

API 设计模式
在设计函数签名时,优先考虑使用 std::expected 作为返回值,而不是 bool 错误码或抛出异常(除非是不可恢复的逻辑错误)。这使得你的 API 意图清晰,调用者可以利用 mapand_then 轻松构建健壮的逻辑。

// 不推荐(错误码可能被忽略,且无错误信息)
bool TryOpenDatabase(const std::string& conn_str, Database& db_out);

// 不推荐(可能抛出未知异常,不易追踪和局部处理)
Database OpenDatabase(const std::string& conn_str); // 抛出 DatabaseException

// 推荐(清晰表达成功与失败,并提供错误信息)
std::expected<Database, DatabaseError> OpenDatabase(const std::string& conn_str);

通过这种方式,你的代码库将变得更加健壮、易于理解和维护,同时还能享受到 std::expected 带来的性能优势。

std::expected 是 C++23 中一个令人兴奋的特性,它为现代 C++ 提供了优雅、类型安全且高效的错误处理方案。通过将成功值和错误值封装在同一类型中,它强制调用者处理两种可能性,避免了错误被静默忽略。其丰富的成员函数,如 mapand_thenor_else,使得链式操作和错误恢复变得异常简洁。在成功路径上,std::expected 能够实现与直接返回值几乎相同的 0 运行时开销,使其成为性能敏感应用场景的理想选择。拥抱 std::expected,让我们的 C++ 代码更加健壮和富有表现力!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注