C++23 std::expected:当函数可能失败时,给它一个优雅的解释机会

各位同仁,各位编程领域的探索者们:

欢迎来到今天的讲座。我们将深入探讨C++23标准库中一个备受期待的特性——std::expected。当函数可能失败时,我们如何优雅地处理这些意料之中的失败,并给予它们一个清晰的解释?这正是std::expected的使命。它不仅仅是一个工具,更是一种设计哲学,旨在提升我们C++代码的健壮性、可读性和可维护性。

一、迷雾中的指引:C++传统错误处理的困境

在C++的世界里,错误处理是一个永恒的话题。从C语言时代继承而来的错误码,到现代C++中广泛使用的异常,再到近年兴起的std::optional,每一种机制都有其设计哲学和适用场景。然而,它们各自的局限性,也常常让我们在追求代码优雅与效率的道路上,感到力不从心。

1.1 错误码:显式与遗漏的矛盾

错误码(或返回状态码)是最古老、最直接的错误处理方式。函数通过返回一个整数或枚举值来指示操作的成功或失败,并在失败时附带一个错误码。

#include <iostream>
#include <string>
#include <system_error> // For std::error_code (though not strictly error code in this example)

enum class ErrorCode {
    Success = 0,
    InvalidArgument,
    FileNotFound,
    PermissionDenied,
    NetworkError
};

// 假设我们有一个函数尝试从文件中读取数据
std::pair<std::string, ErrorCode> readFile(const std::string& path) {
    if (path.empty()) {
        return {"", ErrorCode::InvalidArgument};
    }
    // 模拟文件不存在或权限问题
    if (path == "non_existent.txt") {
        return {"", ErrorCode::FileNotFound};
    }
    if (path == "forbidden.txt") {
        return {"", ErrorCode::PermissionDenied};
    }

    // 模拟成功读取
    return {"Content of " + path, ErrorCode::Success};
}

void processFile_error_code(const std::string& filename) {
    auto [content, err] = readFile(filename);
    if (err != ErrorCode::Success) {
        std::cerr << "Error reading file '" << filename << "': ";
        switch (err) {
            case ErrorCode::InvalidArgument: std::cerr << "Invalid argument.n"; break;
            case ErrorCode::FileNotFound:    std::cerr << "File not found.n"; break;
            case ErrorCode::PermissionDenied:std::cerr << "Permission denied.n"; break;
            case ErrorCode::NetworkError:    std::cerr << "Network error (not simulated).n"; break;
            default:                         std::cerr << "Unknown error.n"; break;
        }
        return;
    }
    std::cout << "Successfully read file '" << filename << "':n" << content << "n";
}

// int main() {
//     processFile_error_code("data.txt");
//     processFile_error_code("non_existent.txt");
//     processFile_error_code("forbidden.txt");
//     processFile_error_code("");
//     return 0;
// }

优点:

  • 显式性: 返回值明确指示了函数的成功或失败状态,需要调用者主动检查。
  • 性能: 没有异常处理的堆栈展开开销,在性能敏感的场景下表现优秀。
  • 确定性: 控制流清晰,易于理解。

缺点:

  • 容易忽略: 调用者可能忘记检查错误码,导致未处理的错误。
  • 返回值污染: 如果函数本身有返回值,错误码需要通过额外的参数(输出参数)或std::pair/std::tuple来传递,这会使函数签名变得复杂。
  • 错误信息有限: 通常只能返回一个简单的数字,难以携带丰富的上下文信息。
  • 样板代码: 每一层都需要检查错误码并决定如何处理或向上冒泡,导致大量重复的if (err != Success)样板代码。
  • 链式调用困难: 难以优雅地进行一系列可能失败的操作。

1.2 异常:优雅与隐晦的权衡

异常是现代C++处理错误的主要机制。它允许我们将正常的执行路径与错误处理路径清晰地分离。当错误发生时,函数可以抛出一个异常,由调用栈上更高层的catch块来捕获和处理。

#include <iostream>
#include <string>
#include <stdexcept> // For standard exceptions

// 自定义异常类型以携带更多信息
class FileOperationError : public std::runtime_error {
public:
    enum class ErrorCode {
        InvalidArgument,
        FileNotFound,
        PermissionDenied,
        NetworkError
    };

    FileOperationError(ErrorCode code, const std::string& msg)
        : std::runtime_error(msg), code_(code) {}

    ErrorCode getErrorCode() const { return code_; }

private:
    ErrorCode code_;
};

std::string readFile_exceptions(const std::string& path) {
    if (path.empty()) {
        throw FileOperationError(FileOperationError::ErrorCode::InvalidArgument, "Path cannot be empty.");
    }
    if (path == "non_existent.txt") {
        throw FileOperationError(FileOperationError::ErrorCode::FileNotFound, "File not found.");
    }
    if (path == "forbidden.txt") {
        throw FileOperationError(FileOperationError::ErrorCode::PermissionDenied, "Permission denied for file.");
    }

    return "Content of " + path;
}

void processFile_exceptions(const std::string& filename) {
    try {
        std::string content = readFile_exceptions(filename);
        std::cout << "Successfully read file '" << filename << "':n" << content << "n";
    } catch (const FileOperationError& e) {
        std::cerr << "Error reading file '" << filename << "': ";
        switch (e.getErrorCode()) {
            case FileOperationError::ErrorCode::InvalidArgument: std::cerr << "Invalid argument.n"; break;
            case FileOperationError::ErrorCode::FileNotFound:    std::cerr << "File not found.n"; break;
            case FileOperationError::ErrorCode::PermissionDenied:std::cerr << "Permission denied.n"; break;
            case FileOperationError::ErrorCode::NetworkError:    std::cerr << "Network error (not simulated).n"; break;
            default:                                             std::cerr << "Unknown error.n"; break;
        }
        std::cerr << "Details: " << e.what() << "n";
    } catch (const std::exception& e) {
        std::cerr << "An unexpected standard error occurred: " << e.what() << "n";
    } catch (...) {
        std::cerr << "An unknown error occurred.n";
    }
}

// int main() {
//     processFile_exceptions("data.txt");
//     processFile_exceptions("non_existent.txt");
//     processFile_exceptions("forbidden.txt");
//     processFile_exceptions("");
//     return 0;
// }

优点:

  • 分离关注点: 正常逻辑与错误处理逻辑分离,代码更清晰。
  • 自动传播: 异常可以自动沿着调用栈向上冒泡,直到被捕获,无需在每一层手动检查。
  • 丰富信息: 异常对象可以携带任意丰富的错误上下文信息。
  • 构造函数: 构造函数无法返回错误码,但可以抛出异常。

缺点:

  • 隐式控制流: 抛出异常会改变正常的控制流,可能难以追踪,尤其是在大型项目中。调用者难以仅通过函数签名判断是否会抛出异常。
  • 性能开销: 异常的抛出和捕获涉及堆栈展开,通常比错误码有更高的性能开销。在某些场景下,这可能是不可接受的。
  • 异常安全: 编写异常安全的代码需要额外的注意和设计,否则可能导致资源泄露或状态不一致。
  • "非预期"的滥用: 异常机制本应用于处理“异常情况”,即那些不经常发生、无法通过正常逻辑恢复的错误。但有时,开发者会将其用于处理“预期失败”,如用户输入错误、文件不存在等,这会增加性能开销和代码复杂性。

1.3 std::optional:值的缺失,但原因不明

C++17引入的std::optional<T>提供了一种表示“可能存在T类型值”或“T类型值不存在”的方式。它解决了函数无法返回有效值时的场景,例如查找一个不存在的元素。

#include <iostream>
#include <string>
#include <optional>

// 查找一个名字,如果找到则返回年龄,否则不返回
std::optional<int> findAge(const std::string& name) {
    if (name == "Alice") return 30;
    if (name == "Bob") return 25;
    return std::nullopt; // 表示没有找到
}

void processAge(const std::string& name) {
    std::optional<int> age = findAge(name);
    if (age) { // 或者 age.has_value()
        std::cout << name << "'s age is: " << *age << "n"; // 或者 age.value()
    } else {
        std::cout << "Could not find age for " << name << ".n";
    }
}

// int main() {
//     processAge("Alice");
//     processAge("Charlie");
//     return 0;
// }

优点:

  • 清晰表达: 明确表示一个值可能缺失。
  • 类型安全: 避免了使用nullptr或特殊值(如-1)来表示缺失,减少了错误。
  • 组合性: 可以与其他C++17特性(如结构化绑定)很好地结合。

缺点:

  • 原因缺失: std::optional只能告诉我们“为什么没有值”,但不能告诉我们“为什么没有值”。例如,findAge函数返回std::nullopt可能是因为名字不存在,也可能是因为数据库连接失败,optional无法区分这些情况。

1.4 总结:我们需要什么?

特性 优点 缺点 适用场景
错误码 显式、性能高、控制流清晰 易忽略、返回值污染、信息有限、样板代码 性能敏感、错误类型简单、非关键路径
异常 关注点分离、自动传播、信息丰富 隐式控制流、性能开销、异常安全复杂、易滥用 异常情况、构造函数失败、无法恢复的错误
optional 清晰表达值缺失、类型安全 无法说明值缺失的原因 值可能不存在,且不存在的原因不重要时

我们真正需要的是一种机制,它能够:

  1. 显式地表示函数可能成功并返回一个值,或者失败并返回一个具体的错误原因。
  2. 避免异常的性能开销和隐式控制流,特别是对于“预期失败”的情况。
  3. 提供比简单错误码更丰富的错误信息。
  4. 支持优雅的链式调用和组合操作,减少样板代码。
  5. std::optional互补,解决其无法表达失败原因的痛点。

这就是std::expected应运而生的背景。

二、std::expected<T, E>:预期的成功或意料中的失败

std::expected<T, E>是C++23标准库引入的一个类型,它表示一个操作可能成功并产生一个T类型的值,或者失败并产生一个E类型的错误。其核心思想是,将成功值和错误值都作为返回值的一部分,迫使调用者处理这两种可能性。

2.1 核心概念

std::expected<T, E>可以被认为是std::optional<T>的增强版。

  • 如果操作成功,它内部存储一个T类型的值。
  • 如果操作失败,它内部存储一个E类型的值。
  • T是成功时返回值的类型,E是失败时错误信息的类型。E不能是引用类型或void

2.2 基本用法

要使用std::expected,你需要包含<expected>头文件。

#include <iostream>
#include <string>
#include <expected> // C++23

// 定义一个自定义错误类型,通常是一个枚举或结构体
enum class DivideByZeroError {
    DivisionByZero,
    // ... 其他可能的错误
};

// 一个可能失败的除法函数
std::expected<int, DivideByZeroError> safeDivide(int numerator, int denominator) {
    if (denominator == 0) {
        return std::unexpected(DivideByZeroError::DivisionByZero); // 返回错误
    }
    return numerator / denominator; // 返回成功值
}

void demo_basic_expected() {
    // 成功的情况
    std::expected<int, DivideByZeroError> result1 = safeDivide(10, 2);
    if (result1.has_value()) { // 检查是否包含值
        std::cout << "10 / 2 = " << result1.value() << "n"; // 获取值
    } else {
        // unreachable
        std::cerr << "Error: " << static_cast<int>(result1.error()) << "n"; // 获取错误
    }

    // 失败的情况
    std::expected<int, DivideByZeroError> result2 = safeDivide(10, 0);
    if (result2.has_value()) {
        // unreachable
        std::cout << "10 / 0 = " << result2.value() << "n";
    } else {
        std::cerr << "Error during 10 / 0: ";
        if (result2.error() == DivideByZeroError::DivisionByZero) {
            std::cerr << "Division by zero!n";
        } else {
            std::cerr << "Unknown error.n";
        }
    }

    // 也可以使用 operator bool()
    if (result1) { // 等同于 result1.has_value()
        std::cout << "10 / 2 (bool check) = " << *result1 << "n"; // 使用解引用运算符
    }

    if (!result2) { // 等同于 !result2.has_value()
        std::cerr << "10 / 0 (bool check) failed.n";
    }
}

// int main() {
//     demo_basic_expected();
//     return 0;
// }

关键成员函数:

  • has_value(): 返回true如果包含值,false如果包含错误。
  • operator bool(): 等同于has_value()
  • value(): 返回包含的值。如果!has_value(),则抛出std::bad_expected_access<E>异常。
  • operator*(): 解引用运算符,返回包含的值。如果!has_value(),则行为未定义。
  • operator->(): 箭头运算符,返回指向包含值的指针。如果!has_value(),则行为未定义。
  • error(): 返回包含的错误。如果has_value(),则抛出std::bad_expected_access<E>异常。
  • value_or(U&& default_value): 如果包含值,返回该值;否则返回default_value
  • operator=(const expected&)operator=(expected&&): 赋值运算符。
  • emplace(Args&&...): 原地构造T类型的值。
  • template<class G> expected<T, G> transform_error(F&& f): 将错误值E转换为G
  • template<class U> expected<U, E> transform(F&& f): 将成功值T转换为U

2.3 std::unexpected<E>std::unexpect

为了区分返回成功值T和返回错误值E,当函数返回错误时,我们需要用std::unexpected<E>来包装错误。
std::unexpected<E>是一个简单的包装器,它的构造函数接受一个E类型的参数。

// 示例:返回一个字符串作为错误信息
std::expected<int, std::string> parseNumber(const std::string& s) {
    try {
        size_t pos;
        int value = std::stoi(s, &pos);
        if (pos != s.length()) {
            return std::unexpected("Invalid characters after number.");
        }
        return value;
    } catch (const std::invalid_argument&) {
        return std::unexpected("Input is not a valid number.");
    } catch (const std::out_of_range&) {
        return std::unexpected("Number is out of range.");
    }
}

void demo_string_error() {
    auto res1 = parseNumber("123");
    if (res1) {
        std::cout << "Parsed: " << *res1 << "n";
    }

    auto res2 = parseNumber("abc");
    if (!res2) {
        std::cerr << "Error parsing 'abc': " << res2.error() << "n";
    }

    auto res3 = parseNumber("123xyz");
    if (!res3) {
        std::cerr << "Error parsing '123xyz': " << res3.error() << "n";
    }
}

// int main() {
//     demo_string_error();
//     return 0;
// }

std::unexpect_t是一个空标签类型,用于在构造std::expected时明确指示我们正在构造一个错误状态,特别是当TE的类型可能导致构造函数重载歧义时。
例如,如果TintEint,直接return 5;会构造一个成功的值,而return std::unexpected(10);会构造一个错误。当E的类型可以隐式转换为T时,std::unexpect_t可以强制选择错误构造函数。不过,对于大多数场景,std::unexpected的显式构造已经足够清晰。

2.4 丰富错误信息:自定义错误类型

使用std::string作为错误类型简单直观,但它通常不足以传递丰富的错误上下文。更强大的方式是定义一个自定义的错误结构体或类。

#include <iostream>
#include <string>
#include <expected>
#include <vector>
#include <format> // C++20 for std::format, useful for error messages

// 自定义错误结构体
struct CustomError {
    enum class Code {
        InvalidArgument,
        FileNotFound,
        PermissionDenied,
        NetworkFailure,
        ParsingFailed,
        Unknown
    };

    Code code;
    std::string message;
    std::string context; // 附加上下文信息,如文件名、行号等

    // 方便的工厂方法
    static CustomError invalidArgument(std::string_view msg, std::string_view ctx = "") {
        return {Code::InvalidArgument, std::string(msg), std::string(ctx)};
    }
    static CustomError fileNotFound(std::string_view path) {
        return {Code::FileNotFound, std::format("File not found: {}", path), std::string(path)};
    }
    // ... 其他错误类型的工厂方法
};

// 假设有一个函数从文件加载配置
std::expected<std::string, CustomError> loadConfigFile(const std::string& path) {
    if (path.empty()) {
        return std::unexpected(CustomError::invalidArgument("Config path cannot be empty."));
    }
    if (path == "non_existent_config.json") {
        return std::unexpected(CustomError::fileNotFound(path));
    }
    if (path == "permissions_denied.json") {
        return std::unexpected({CustomError::Code::PermissionDenied, "Access to config file denied.", path});
    }
    // 模拟成功读取
    return std::format("Content of config file '{}'", path);
}

void demo_custom_error_type() {
    auto config1 = loadConfigFile("my_config.json");
    if (config1) {
        std::cout << "Config loaded: " << *config1 << "n";
    }

    auto config2 = loadConfigFile("non_existent_config.json");
    if (!config2) {
        std::cerr << "Error loading config: ";
        switch (config2.error().code) {
            case CustomError::Code::FileNotFound:
                std::cerr << "File not found! Path: " << config2.error().context << "n";
                break;
            case CustomError::Code::InvalidArgument:
                std::cerr << "Invalid argument! Message: " << config2.error().message << "n";
                break;
            case CustomError::Code::PermissionDenied:
                std::cerr << "Permission denied! Message: " << config2.error().message << "n";
                break;
            default:
                std::cerr << "Unknown error: " << config2.error().message << "n";
                break;
        }
    }

    auto config3 = loadConfigFile("");
    if (!config3) {
        std::cerr << "Error loading config: " << config3.error().message << "n";
    }
}

// int main() {
//     demo_custom_error_type();
//     return 0;
// }

通过自定义错误类型,我们可以携带任意多的上下文信息,极大地提高了错误的可诊断性。

三、链式调用与函数式范式:std::expected的真正威力

std::expected的强大之处远不止于简单的成功/失败判断。它借鉴了函数式编程中的Monad概念,提供了一系列链式操作,使得处理一系列可能失败的步骤变得异常优雅。

3.1 and_then:串联可能失败的操作

and_then用于连接一系列返回std::expected的函数。如果当前的std::expected是成功状态,它会将成功值传递给下一个函数;如果当前是错误状态,它会直接跳过后续函数,传播错误。

#include <iostream>
#include <string>
#include <expected>
#include <charconv> // For std::from_chars
#include <vector>

// 假设我们有更精细的错误类型
struct ParseError {
    enum class Code { InvalidFormat, OutOfRange, EmptyInput };
    Code code;
    std::string message;
};

// 1. 将字符串解析为整数
std::expected<int, ParseError> parseInt(std::string_view s) {
    if (s.empty()) {
        return std::unexpected({ParseError::Code::EmptyInput, "Input string is empty."});
    }
    int value;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec == std::errc()) {
        if (ptr != s.data() + s.size()) {
            return std::unexpected({ParseError::Code::InvalidFormat, "Contains non-numeric characters."});
        }
        return value;
    } else if (ec == std::errc::invalid_argument) {
        return std::unexpected({ParseError::Code::InvalidFormat, "Cannot convert to integer."});
    } else if (ec == std::errc::result_out_of_range) {
        return std::unexpected({ParseError::Code::OutOfRange, "Integer out of range."});
    }
    return std::unexpected({ParseError::Code::InvalidFormat, "Unknown parse error."});
}

// 2. 验证整数是否在某个范围内
struct ValidationError {
    enum class Code { OutOfBounds, NegativeNotAllowed };
    Code code;
    std::string message;
};

std::expected<int, ValidationError> validatePositive(int num) {
    if (num < 0) {
        return std::unexpected({ValidationError::Code::NegativeNotAllowed, "Number must be positive."});
    }
    return num;
}

// 3. 对整数进行平方操作
// 注意:这个函数不返回 expected,但它的结果可以被 transform 处理
int square(int num) {
    return num * num;
}

// 4. 将整数转换为字符串
std::expected<std::string, std::runtime_error> intToString(int num) {
    return std::to_string(num);
}

void demo_and_then() {
    std::cout << "--- Demo: and_then ---n";

    // 成功路径
    auto result1 = parseInt("10")
        .and_then([](int i) { return validatePositive(i); })
        .transform([](int i) { return square(i); }) // transform 接受普通函数
        .and_then([](int i) { return intToString(i); }); // and_then 接受返回 expected 的函数

    if (result1) {
        std::cout << "Success path: " << *result1 << "n"; // Output: Success path: 100
    } else {
        std::cerr << "Error path 1: " << result1.error().what() << "n"; // Note: error() here would be a variant/custom type
    }

    // 失败路径 1: 解析失败
    auto result2 = parseInt("-5a")
        .and_then([](int i) { return validatePositive(i); })
        .transform([](int i) { return square(i); })
        .and_then([](int i) { return intToString(i); });

    if (!result2) {
        // result2 的错误类型是 ParseError,因为 parseInt 失败
        std::cerr << "Error path 2 (ParseError): " << result2.error().message << "n"; // Output: Error path 2 (ParseError): Contains non-numeric characters.
    }

    // 失败路径 2: 验证失败
    auto result3 = parseInt("-5")
        .and_then([](int i) { return validatePositive(i); })
        .transform([](int i) { return square(i); })
        .and_then([](int i) { return intToString(i); });

    if (!result3) {
        // result3 的错误类型是 ValidationError,因为 validatePositive 失败
        std::cerr << "Error path 3 (ValidationError): " << result3.error().message << "n"; // Output: Error path 3 (ValidationError): Number must be positive.
    }
}

// int main() {
//     demo_and_then();
//     return 0;
// }

注意,and_then的lambda表达式必须返回一个std::expected类型。如果你的下一个操作不返回std::expected,而是返回一个普通值,你需要使用transform

3.2 transform:转换成功的值

transform用于在成功状态下,将std::expected内部的值从T类型转换为U类型,而错误类型保持不变。如果当前是错误状态,transform会直接传播错误。

// 示例:将解析出的整数转换为其字符串表示
void demo_transform() {
    std::cout << "n--- Demo: transform ---n";

    auto res1 = parseInt("42")
        .transform([](int i) { return std::to_string(i) + " degrees"; }); // int -> string

    if (res1) {
        std::cout << "Transformed value: " << *res1 << "n"; // Output: Transformed value: 42 degrees
    }

    auto res2 = parseInt("xyz")
        .transform([](int i) { return std::to_string(i) + " degrees"; });

    if (!res2) {
        std::cerr << "Transform skipped due to error: " << res2.error().message << "n"; // Output: Transform skipped due to error: Cannot convert to integer.
    }
}
// int main() {
//     demo_transform();
//     return 0;
// }

3.3 or_else:错误恢复或转换

or_else用于在std::expected处于错误状态时,尝试进行错误恢复或将错误转换为另一种错误类型。如果当前是成功状态,or_else会直接传播成功值。

#include <chrono> // For std::chrono
#include <thread> // For std::this_thread::sleep_for

// 模拟一个可能因网络问题失败的函数
struct NetworkError {
    enum class Code { Timeout, Disconnected, ServerError };
    Code code;
    std::string message;
};

std::expected<std::string, NetworkError> fetchDataFromPrimaryServer(const std::string& query) {
    if (query == "critical_data") {
        std::cout << "Primary server: Simulating network timeout...n";
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟延迟
        return std::unexpected({NetworkError::Code::Timeout, "Primary server timed out."});
    }
    return std::format("Data from primary for '{}'", query);
}

std::expected<std::string, NetworkError> fetchDataFromBackupServer(const std::string& query) {
    std::cout << "Backup server: Attempting to fetch data...n";
    if (query == "critical_data") {
        // 备份服务器也可能失败,但这里假设它成功了
        return std::format("Data from backup for '{}'", query);
    }
    return std::unexpected({NetworkError::Code::Disconnected, "Backup server disconnected for non-critical data."});
}

void demo_or_else() {
    std::cout << "n--- Demo: or_else ---n";

    // 场景1: Primary失败,Backup成功
    auto data1 = fetchDataFromPrimaryServer("critical_data")
        .or_else([](NetworkError err) {
            std::cerr << "Primary server failed: " << err.message << ". Trying backup...n";
            return fetchDataFromBackupServer("critical_data");
        });

    if (data1) {
        std::cout << "Fetched data: " << *data1 << "n"; // Output: Data from backup for 'critical_data'
    } else {
        std::cerr << "Failed to fetch data from both servers: " << data1.error().message << "n";
    }

    // 场景2: Primary成功
    auto data2 = fetchDataFromPrimaryServer("non_critical_data")
        .or_else([](NetworkError err) {
            std::cerr << "Primary server failed: " << err.message << ". Trying backup...n";
            return fetchDataFromBackupServer("non_critical_data");
        });

    if (data2) {
        std::cout << "Fetched data: " << *data2 << "n"; // Output: Data from primary for 'non_critical_data'
    } else {
        std::cerr << "Failed to fetch data from both servers: " << data2.error().message << "n";
    }

    // 场景3: Both fail
    auto data3 = fetchDataFromPrimaryServer("very_critical_data") // Assume this fails with a timeout
        .or_else([](NetworkError err) {
            std::cerr << "Primary server failed for very_critical_data: " << err.message << ". Trying backup...n";
            // Assume backup also fails for this specific query
            return std::unexpected({NetworkError::Code::ServerError, "Backup server also failed."});
        });

    if (!data3) {
        std::cerr << "Failed to fetch data from both servers: " << data3.error().message << "n";
    }
}

// int main() {
//     demo_or_else();
//     return 0;
// }

or_else的lambda表达式必须返回一个std::expected类型,其成功值类型与原expected相同,错误值类型则可以不同(但通常相同)。

3.4 transform_error:转换错误信息

transform_error用于在错误状态下,将std::expected内部的错误从E类型转换为F类型,而成功值类型保持不变。如果当前是成功状态,transform_error会直接传播成功值。

#include <exception> // For std::exception
#include <system_error> // For std::error_code

// 一个更通用的错误报告结构
struct ReportError {
    int severity;
    std::string description;
    std::string component;
};

// 将 ParseError 转换为 ReportError
ReportError mapParseErrorToReportError(ParseError err) {
    std::string desc;
    int sev = 1; // Default severity
    switch (err.code) {
        case ParseError::Code::EmptyInput:
            desc = "Input was empty, nothing to parse.";
            sev = 2;
            break;
        case ParseError::Code::InvalidFormat:
            desc = "Invalid format encountered during parsing.";
            sev = 3;
            break;
        case ParseError::Code::OutOfRange:
            desc = "Parsed value was out of expected range.";
            sev = 4;
            break;
    }
    return {sev, std::format("{}: {}", desc, err.message), "Parser"};
}

void demo_transform_error() {
    std::cout << "n--- Demo: transform_error ---n";

    auto res1 = parseInt("abc")
        .transform_error(mapParseErrorToReportError);

    if (!res1) {
        ReportError err = res1.error();
        std::cerr << "ReportError from parsing: Severity=" << err.severity
                  << ", Component=" << err.component
                  << ", Description=" << err.description << "n";
    }

    auto res2 = parseInt("123")
        .transform_error(mapParseErrorToReportError); // 成功时 transform_error 不会执行

    if (res2) {
        std::cout << "Successfully parsed and value is: " << *res2 << "n";
    }
}

// int main() {
//     demo_transform_error();
//     return 0;
// }

3.5 value_or:提供默认值

value_or方法提供了一个方便的方式来处理错误,如果expected包含一个值,则返回该值;否则,返回一个提供的默认值。这类似于std::optional::value_or

void demo_value_or() {
    std::cout << "n--- Demo: value_or ---n";

    int val1 = parseInt("100").value_or(-1); // Success
    std::cout << "Parsed 100 or default: " << val1 << "n"; // Output: 100

    int val2 = parseInt("invalid").value_or(-1); // Failure
    std::cout << "Parsed 'invalid' or default: " << val2 << "n"; // Output: -1
}

// int main() {
//     demo_value_or();
//     return 0;
// }

3.6 Monadic Operations 总结

方法 输入 (expected<T, E>) 行为 返回 (expected<U, F>)
and_then(F) T T应用函数FF返回expected<U, E> expected<U, E>
E 直接传播E expected<U, E> (错误状态)
transform(F) T T应用函数FF返回U expected<U, E>
E 直接传播E expected<U, E> (错误状态)
or_else(F) T 直接传播T expected<T, F> (成功状态)
E E应用函数FF返回expected<T, F> expected<T, F>
transform_error(F) T 直接传播T expected<T, F> (成功状态)
E E应用函数FF返回F' expected<T, F'>
value_or(U) T 返回T TU (不是expected类型)
E 返回U TU (不是expected类型)

这些操作是std::expected实现其链式调用和函数式编程风格的关键。它们使得复杂的错误处理逻辑能够以声明式、流畅的方式表达,极大地提高了代码的可读性和简洁性。

四、std::expected在实践中的设计模式与最佳实践

理解std::expected的机制只是第一步,更重要的是如何在实际项目中有效地运用它。这涉及到何时使用、如何设计错误类型、以及如何与其他C++特性协同工作。

4.1 何时使用std::expected vs. 异常

这是一个核心的设计决策。一般原则是:

  • 使用std::expected处理“预期失败”(Expected Failures):

    • 这些错误是函数设计中考虑到的、可能发生的、并且通常可以局部处理或向上层报告的失败。
    • 例如:用户输入验证失败、文件不存在、网络连接超时、数据库查询无结果、解析错误、资源暂时不可用。
    • 这些情况下,失败是函数契约的一部分,调用者应该被强制处理。
    • 性能开销相对较低,因为没有堆栈展开。
  • 使用异常处理“非预期失败”(Exceptional Failures):

    • 这些错误是程序逻辑上的缺陷、系统资源的耗尽、或无法通过正常手段恢复的严重错误。
    • 例如:内存分配失败(std::bad_alloc)、数组越界(除非是编程错误)、不可恢复的硬件错误、不变量被破坏。
    • 这些错误通常意味着程序无法继续以有意义的方式运行,或者需要终止。
    • 异常的堆栈展开机制对于这些“灾难性”事件来说是合适的。

总结表格:

特性 std::expected 异常
场景 预期失败,函数契约的一部分,可恢复或报告 非预期失败,程序逻辑错误,通常不可恢复
显式性 显式要求调用者处理成功或失败路径 隐式抛出,调用者可能不知道或忘记捕获
控制流 基于值传递的清晰控制流 通过堆栈展开改变控制流,可能难以追踪
性能 通常更优,无堆栈展开开销 抛出时开销较大,涉及堆栈展开
错误信息 通过E类型携带丰富信息,类型安全 通过异常对象携带丰富信息,类型安全
链式调用 借助and_then/transform等实现流畅的链式操作 难以直接链式调用,需要try-catch块嵌套
构造函数 不适用,但可处理构造函数可能失败的场景(通过工厂函数返回expected 唯一能从构造函数报告失败的机制(抛出异常)

4.2 设计你的错误类型 E

E类型是std::expected中传递错误信息的载体,其设计至关重要。

  1. 简单错误: 对于非常简单的错误,一个枚举或std::string可能就足够了。

    enum class NetworkError { Timeout, Disconnected };
    std::expected<Data, NetworkError> fetch();
  2. 丰富上下文错误: 大多数实际场景需要更详细的信息。使用结构体或类来封装错误码、消息、发生位置(文件名、行号)、时间戳、相关数据等。

    struct FileError {
        enum class Code { NotFound, PermissionDenied, Corrupted };
        Code code;
        std::string path;
        std::string message;
        // Optional: std::source_location for C++20
    };
    std::expected<FileContent, FileError> readFile(const std::string& path);
  3. 组合错误:std::variant<ErrorType1, ErrorType2, ...>
    当一个函数可能返回多种不同类别的错误时,std::variant是一个非常强大的选择。

    #include <variant>
    
    struct ParseError { /* ... */ };
    struct FileError { /* ... */ };
    struct NetworkError { /* ... */ };
    
    using MyCompositeError = std::variant<ParseError, FileError, NetworkError>;
    
    std::expected<Config, MyCompositeError> loadAndParseConfig(const std::string& path, const std::string& url) {
        // ... file operations ...
        if (file_error) {
            return std::unexpected(FileError{/* ... */});
        }
    
        // ... network operations ...
        if (network_error) {
            return std::unexpected(NetworkError{/* ... */});
        }
    
        // ... parsing operations ...
        if (parse_error) {
            return std::unexpected(ParseError{/* ... */});
        }
    
        return Config{};
    }
    
    // 处理时可以使用 std::visit
    void handleCompositeError(MyCompositeError err) {
        std::visit([](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, ParseError>) {
                std::cerr << "Parsing error: " << arg.message << "n";
            } else if constexpr (std::is_same_v<T, FileError>) {
                std::cerr << "File error: " << arg.path << " - " << arg.message << "n";
            } else if constexpr (std::is_same_v<T, NetworkError>) {
                std::cerr << "Network error: " << arg.message << "n";
            }
        }, err);
    }
  4. std::error_codestd::error_condition
    对于系统级或库定义的错误,C++标准库提供了std::error_codestd::error_condition。它们与操作系统错误码或协议错误码集成,提供了跨平台的错误表示。

    #include <system_error>
    
    std::expected<std::string, std::error_code> readFromSocket(int socket_fd) {
        // ... read op ...
        if (read_failed) {
            return std::unexpected(std::make_error_code(std::errc::connection_aborted));
        }
        return "data";
    }
    
    void processSocketRead() {
        auto res = readFromSocket(123);
        if (!res) {
            std::error_code ec = res.error();
            std::cerr << "Socket read error: " << ec.message() << " (category: " << ec.category().name() << ")n";
        }
    }

4.3 零开销抽象:性能考虑

std::expected被设计为零开销抽象。

  • 它通常与TE类型本身的大小大致相同,加上一个bool标志位(或通过TE的填充字节来优化存储)。
  • 它避免了堆上的额外内存分配,除非TE自身在内部执行分配。
  • 对于预期失败,它避免了异常处理的堆栈展开开销。
  • 其 monadic 操作通常通过内联和移动语义进行优化,避免不必要的复制。

然而,需要注意的是,如果你的TE类型很大且昂贵,那么std::expected的复制或移动仍然会有开销。在这种情况下,考虑使用智能指针(如std::unique_ptr<T>)作为T,或者在E中只存储轻量级的错误标识,而不是完整的错误对象副本。

4.4 构造函数和工厂函数

构造函数不能返回std::expected。如果一个对象的构造可能失败,最佳实践是使用一个静态工厂函数来代替:

class DatabaseConnection {
private:
    DatabaseConnection() = default; // 私有构造函数,强制使用工厂方法
    // ... private members ...

public:
    struct ConnectError {
        enum class Code { InvalidCredentials, NetworkIssue, Timeout };
        Code code;
        std::string message;
    };

    static std::expected<DatabaseConnection, ConnectError> create(
        const std::string& host, int port, const std::string& user, const std::string& pass) {

        // 模拟连接失败
        if (host == "badhost") {
            return std::unexpected({ConnectError::Code::NetworkIssue, "Could not reach host."});
        }
        if (user == "invalid") {
            return std::unexpected({ConnectError::Code::InvalidCredentials, "Invalid username."});
        }

        // 成功连接,构造对象
        DatabaseConnection conn;
        // ... initialize conn ...
        std::cout << "Database connection established.n";
        return conn;
    }

    void query(const std::string& sql) {
        std::cout << "Executing query: " << sql << "n";
    }
};

void demo_factory_function() {
    auto conn1 = DatabaseConnection::create("localhost", 5432, "admin", "pass");
    if (conn1) {
        conn1->query("SELECT * FROM users;");
    } else {
        std::cerr << "Failed to connect to DB: " << conn1.error().message << "n";
    }

    auto conn2 = DatabaseConnection::create("badhost", 5432, "admin", "pass");
    if (!conn2) {
        std::cerr << "Failed to connect to DB: " << conn2.error().message << "n";
    }
}

// int main() {
//     demo_factory_function();
//     return 0;
// }

4.5 整合到现有代码

当旧代码返回错误码或抛出异常时,如何将其与std::expected集成?

  • 错误码转expected 编写一个包装函数,将错误码转换为std::expected

    // Old C-style API
    int old_api_read_file(const char* path, char* buffer, size_t max_len); // Returns 0 on success, <0 on error
    
    std::expected<std::string, int> wrapped_read_file(const std::string& path) {
        std::vector<char> buffer(1024);
        int result = old_api_read_file(path.c_str(), buffer.data(), buffer.size());
        if (result == 0) {
            return std::string(buffer.data());
        } else {
            return std::unexpected(result); // Return the raw error code
        }
    }
  • 异常转expected 使用try-catch块将可能抛出异常的函数包装起来,将其结果转换为std::expected

    // Old C++ API that throws
    std::string old_api_parse_json(const std::string& json_str); // Throws JsonParseException
    
    struct JsonParseError { std::string what; };
    
    std::expected<std::string, JsonParseError> wrapped_parse_json(const std::string& json_str) {
        try {
            return old_api_parse_json(json_str);
        } catch (const JsonParseException& e) {
            return std::unexpected({e.what()});
        } catch (const std::exception& e) {
            return std::unexpected({"Unexpected error: " + std::string(e.what())});
        }
    }

4.6 实例:配置解析器

让我们通过一个更复杂的例子来演示std::expected的实际应用。我们将构建一个简单的配置解析器,它从文件读取内容,解析键值对,并进行一些验证。

#include <iostream>
#include <string>
#include <expected>
#include <vector>
#include <map>
#include <fstream>
#include <sstream>
#include <charconv> // For std::from_chars (C++17)
#include <algorithm> // For std::remove_if
#include <stdexcept> // For std::runtime_error

// --- 1. 定义错误类型 ---
struct ConfigError {
    enum class Code {
        FileNotFound,
        PermissionDenied,
        EmptyPath,
        InvalidFormat,
        DuplicateKey,
        ParseIntFailed,
        ValidationFailed,
        Unknown
    };

    Code code;
    std::string message;
    std::string context; // e.g., filename, line number, key name

    static ConfigError fileNotFound(std::string_view path) {
        return {Code::FileNotFound, std::format("File not found: {}", path), std::string(path)};
    }
    static ConfigError emptyPath() {
        return {Code::EmptyPath, "Configuration file path cannot be empty."};
    }
    static ConfigError invalidFormat(std::string_view msg, std::string_view ctx = "") {
        return {Code::InvalidFormat, std::string(msg), std::string(ctx)};
    }
    static ConfigError duplicateKey(std::string_view key) {
        return {Code::DuplicateKey, std::format("Duplicate key found: {}", key), std::string(key)};
    }
    static ConfigError parseIntFailed(std::string_view val, std::string_view key) {
        return {Code::ParseIntFailed, std::format("Failed to parse integer value '{}' for key '{}'", val, key), std::string(key)};
    }
    static ConfigError validationFailed(std::string_view msg, std::string_view key = "") {
        return {Code::ValidationFailed, std::string(msg), std::string(key)};
    }

    std::string toString() const {
        return std::format("[Error Code: {}] Message: {} (Context: {})", static_cast<int>(code), message, context);
    }
};

// --- 2. 配置数据结构 ---
struct AppConfig {
    std::string app_name;
    int max_threads;
    std::string log_file;
    bool debug_mode;

    void print() const {
        std::cout << "--- App Configuration ---n";
        std::cout << "  App Name: " << app_name << "n";
        std::cout << "  Max Threads: " << max_threads << "n";
        std::cout << "  Log File: " << log_file << "n";
        std::cout << "  Debug Mode: " << (debug_mode ? "true" : "false") << "n";
        std::cout << "-------------------------n";
    }
};

// --- 3. 辅助函数:trim string ---
std::string trim(const std::string& str) {
    size_t first = str.find_first_not_of(" tnr");
    if (std::string::npos == first) {
        return str;
    }
    size_t last = str.find_last_not_of(" tnr");
    return str.substr(first, (last - first + 1));
}

// --- 4. 核心功能:读取文件内容 ---
std::expected<std::string, ConfigError> readFileContent(const std::string& path) {
    if (path.empty()) {
        return std::unexpected(ConfigError::emptyPath());
    }
    std::ifstream file(path);
    if (!file.is_open()) {
        // More robust error handling for real systems could check errno
        return std::unexpected(ConfigError::fileNotFound(path));
    }
    // Simulate permission denied for a specific file
    if (path == "forbidden_config.conf") {
        return std::unexpected({ConfigError::Code::PermissionDenied, "Access to file denied.", path});
    }

    std::stringstream buffer;
    buffer << file.rdbuf();
    return buffer.str();
}

// --- 5. 核心功能:解析键值对 ---
std::expected<std::map<std::string, std::string>, ConfigError> parseKeyValuePairs(const std::string& content) {
    std::map<std::string, std::string> config_map;
    std::stringstream ss(content);
    std::string line;
    int line_num = 0;

    while (std::getline(ss, line)) {
        line_num++;
        std::string trimmed_line = trim(line);
        if (trimmed_line.empty() || trimmed_line[0] == '#') { // Skip empty lines and comments
            continue;
        }

        size_t eq_pos = trimmed_line.find('=');
        if (eq_pos == std::string::npos) {
            return std::unexpected(ConfigError::invalidFormat(
                std::format("Missing '=' in line {}: '{}'", line_num, trimmed_line),
                std::format("Line {}", line_num)
            ));
        }

        std::string key = trim(trimmed_line.substr(0, eq_pos));
        std::string value = trim(trimmed_line.substr(eq_pos + 1));

        if (key.empty()) {
             return std::unexpected(ConfigError::invalidFormat(
                std::format("Empty key in line {}: '{}'", line_num, trimmed_line),
                std::format("Line {}", line_num)
            ));
        }
        if (config_map.count(key)) {
            return std::unexpected(ConfigError::duplicateKey(key));
        }
        config_map[key] = value;
    }
    return config_map;
}

// --- 6. 核心功能:从键值对构建 AppConfig 并验证 ---
std::expected<AppConfig, ConfigError> buildAndValidateConfig(const std::map<std::string, std::string>& kv_pairs) {
    AppConfig config;

    // App Name
    auto it_name = kv_pairs.find("app_name");
    if (it_name == kv_pairs.end()) {
        return std::unexpected(ConfigError::validationFailed("Missing 'app_name' key."));
    }
    config.app_name = it_name->second;

    // Max Threads
    auto it_threads = kv_pairs.find("max_threads");
    if (it_threads == kv_pairs.end()) {
        return std::unexpected(ConfigError::validationFailed("Missing 'max_threads' key."));
    }
    int threads_val;
    auto [ptr, ec] = std::from_chars(it_threads->second.data(), it_threads->second.data() + it_threads->second.size(), threads_val);
    if (ec != std::errc() || ptr != it_threads->second.data() + it_threads->second.size()) {
        return std::unexpected(ConfigError::parseIntFailed(it_threads->second, "max_threads"));
    }
    if (threads_val <= 0) {
        return std::unexpected(ConfigError::validationFailed("max_threads must be positive.", "max_threads"));
    }
    config.max_threads = threads_val;

    // Log File
    auto it_log = kv_pairs.find("log_file");
    if (it_log == kv_pairs.end()) {
        return std::unexpected(ConfigError::validationFailed("Missing 'log_file' key."));
    }
    config.log_file = it_log->second;
    if (config.log_file.empty()) {
        return std::unexpected(ConfigError::validationFailed("log_file path cannot be empty.", "log_file"));
    }

    // Debug Mode
    auto it_debug = kv_pairs.find("debug_mode");
    if (it_debug == kv_pairs.end()) {
        return std::unexpected(ConfigError::validationFailed("Missing 'debug_mode' key."));
    }
    std::string debug_str = it_debug->second;
    std::transform(debug_str.begin(), debug_str.end(), debug_str.begin(), ::tolower);
    if (debug_str == "true" || debug_str == "1") {
        config.debug_mode = true;
    } else if (debug_str == "false" || debug_str == "0") {
        config.debug_mode = false;
    } else {
        return std::unexpected(ConfigError::validationFailed("debug_mode must be 'true'/'false' or '1'/'0'.", "debug_mode"));
    }

    return config;
}

// --- 7. 组合所有步骤:Load, Parse, Validate ---
std::expected<AppConfig, ConfigError> loadConfig(const std::string& config_path) {
    return readFileContent(config_path) // Step 1: Read file content
        .and_then([](const std::string& content) { // Step 2: Parse key-value pairs
            return parseKeyValuePairs(content);
        })
        .and_then([](const std::map<std::string, std::string>& kv_pairs) { // Step 3: Build and validate AppConfig
            return buildAndValidateConfig(kv_pairs);
        });
}

// --- 8. 演示函数 ---
void demo_config_parser() {
    std::cout << "n--- Demo: Config Parser ---n";

    // Create a valid config file
    std::ofstream("valid_config.conf") <<
        "app_name = MyAwesomeAppn"
        "max_threads = 8n"
        "log_file = /var/log/myapp.logn"
        "debug_mode = truen";

    // Create an invalid config file (missing key)
    std::ofstream("invalid_config_missing_key.conf") <<
        "app_name = MyBrokenAppn"
        "max_threads = 4n"
        "# Missing log_filen"
        "debug_mode = falsen";

    // Create an invalid config file (bad integer value)
    std::ofstream("invalid_config_bad_int.conf") <<
        "app_name = MyBuggyAppn"
        "max_threads = eightn"
        "log_file = /tmp/log.txtn"
        "debug_mode = truen";

    // Create an invalid config file (duplicate key)
    std::ofstream("invalid_config_duplicate_key.conf") <<
        "app_name = DuplicateKeyAppn"
        "max_threads = 2n"
        "log_file = /tmp/dup.logn"
        "log_file = /tmp/another_dup.logn"
        "debug_mode = falsen";

    // Test cases
    std::vector<std::string> config_files = {
        "valid_config.conf",
        "non_existent.conf",
        "forbidden_config.conf",
        "", // Empty path
        "invalid_config_missing_key.conf",
        "invalid_config_bad_int.conf",
        "invalid_config_duplicate_key.conf"
    };

    for (const auto& file : config_files) {
        std::cout << "nAttempting to load config from: '" << file << "'n";
        auto config_result = loadConfig(file);
        if (config_result) {
            std::cout << "SUCCESS! Config loaded:n";
            config_result->print();
        } else {
            std::cerr << "FAILURE! " << config_result.error().toString() << "n";
        }
    }
}

int main() {
    demo_config_parser();
    return 0;
}

这个配置解析器示例展示了std::expected如何优雅地处理多阶段操作中的各种预期失败。每个阶段的函数都返回std::expected,并通过and_then链式调用。任何一个阶段的失败都会立即传播到最终结果,而无需在每个中间步骤进行繁琐的if-else检查。错误类型ConfigError能够携带丰富的上下文信息,方便诊断。

五、C++23 std::expected的地位与展望

std::expected在C++23中正式标准化,标志着C++在错误处理方面迈出了重要一步。它填补了传统错误码和异常之间的空白,为开发者提供了一个处理预期失败的优雅且高效的工具。

5.1 对C++生态的影响

  1. API设计: std::expected将成为设计现代C++库API的首选方式,用于表示可能失败的操作。
  2. 代码可读性: 强制调用者处理成功和失败两种情况,提高了代码的显式性和可读性。
  3. 互操作性: 作为标准库的一部分,它将促进不同库之间错误处理方式的统一。
  4. 函数式编程范式: 它的 monadic 接口鼓励函数式编程风格,有助于编写更模块化、更易于测试的代码。

5.2 未来可能的演进

虽然std::expected本身在C++23中已经相当完善,但C++语言和标准库仍在不断发展。

  • 模式匹配 (Pattern Matching): 如果C++将来引入更强大的模式匹配机制(类似于Rust的match表达式),那么处理std::expected的成功和失败状态将变得更加简洁和富有表现力。
  • 运算符重载: 社区中曾讨论过为and_then等操作引入更简洁的运算符重载(如>>=),但考虑到C++的复杂性,短期内不太可能。
  • 与协程的集成: std::expected在异步编程和协程中具有巨大潜力。例如,co_await一个返回std::expected的协程,可以在失败时自动传播错误。这已经可以通过自定义协程类型来实现,未来可能在标准库层面有更直接的支持。

六、结语

std::expected是C++23为我们带来的一个深思熟虑的礼物。它不是要取代异常,也不是要彻底摒弃错误码,而是提供了一个强有力的补充,让我们能够针对不同类型的错误选择最合适的处理机制。它以其显式性、类型安全性和优雅的链式操作,赋能我们编写更健壮、更易读、更具表现力的C++代码。掌握std::expected,意味着在现代C++开发中,你将能够以更高的姿态,优雅地驾驭函数可能失败的复杂性,为你的代码赋予更清晰、更负责任的解释机会。

发表回复

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