跳转至

嵌入式C++教程——函数式错误处理模式

引言

写C++的这些年,我见过太多错误的错误处理方式。有人把try-catch当goto用,有人用-1表示所有错误(到底是哪个错误?),有人到处assert然后生产环境直接崩溃。问题的核心不是"要不要处理错误",而是"如何优雅地处理错误"。

函数式编程给了我们一个很好的思路:把错误当成值。不再是异常那种控制流的突然跳转,而是像处理返回值一样处理错误——可以传递、可以转换、可以组合。

我们已经讲过std::optionalstd::expected,这一章把它们串起来,看看如何在实际项目中构建一套函数式风格的错误处理体系。

一句话总结:函数式错误处理把错误当成一等公民,通过组合器(combinator)模式让错误传播和转换变得可预测、可组合,比异常更安全,比错误码更清晰。


传统错误处理的问题

让我们先看看几种常见的错误处理方式,以及它们的痛点:

1. 错误码的泥潭

// 经典的C风格错误处理
int parse_config(const char* path, Config* out_config) {
    FILE* f = fopen(path, "r");
    if (!f) return ERR_FILE_NOT_FOUND;

    char buffer[1024];
    if (fread(buffer, 1, sizeof(buffer), f) == 0) {
        fclose(f);
        return ERR_READ_FAILED;
    }

    // ... 一堆if检查
    if (some_condition) {
        fclose(f);
        return ERR_INVALID_FORMAT;  // 又一个错误码
    }

    fclose(f);
    return 0;  // 成功
}

// 调用者要写一堆if检查
Config cfg;
int err = parse_config("config.txt", &cfg);
if (err != 0) {
    // 但到底是哪个错误?要查头文件里的宏定义
    handle_error(err);
    return err;
}

这种写法有几个问题:

  • 错误码是整数,不知道它代表什么含义
  • 调用者必须记得检查返回值(否则错误被吞掉)
  • 每层都要手动向上传递错误

2. 异常的惊喜感

// 用异常写起来很舒服,但...
Config load_config(const std::string& path) {
    std::ifstream f(path);
    if (!f) throw std::runtime_error("file not found");  // 这里会抛

    // ... 一堆操作
    if (invalid) throw std::runtime_error("invalid format");  // 这里也会抛

    return parse_from_stream(f);  // 别人也可能抛
}

// 调用者不知道哪些地方会抛异常
void init_system() {
    auto cfg = load_config("config.txt");  // 这行可能抛吗?
    apply_config(cfg);  // 这行呢?
    // ... 更多代码
}

异常的问题:

  • 控制流不透明——你不知道哪行会抛
  • 性能不可预测(堆展开、栈展开)
  • 嵌入式系统往往禁用异常

3. 混合方法的灾难

// 有些函数返回错误码,有些抛异常,有些用optional
std::optional<Config> load_config(const std::string& path);  // optional
bool save_config(const Config& cfg, std::string* error);    // 错误码+输出参数
void apply_config(const Config& cfg);                        // 异常

这种API让调用者无所适从,每次都要查文档才知道这个函数怎么处理错误。


函数式错误处理的核心思想

函数式错误处理的核心是:错误是类型系统的一部分,不是控制流的意外

让我们用之前讲过的工具来构建:

#include <optional>
#include <expected>
#include <string>
#include <functional>

// 定义错误类型
enum class ErrorCode {
    FileNotFound,
    InvalidFormat,
    ChecksumMismatch,
    OutOfMemory,
};

// 带有额外信息的错误
struct Error {
    ErrorCode code;
    std::string message;

    // 方便构造
    static Error make(ErrorCode c, std::string msg) {
        return Error{c, std::move(msg)};
    }
};

// 操作成功返回T,失败返回Error
template<typename T>
using Result = std::expected<T, Error>;

// 不返回值的操作
using VoidResult = std::expected<void, Error>;

现在我们有了一个类型安全的错误表示。接下来看看怎么用。


基本模式:Result的传播

最简单的用法是直接检查:

Result<int> parse_int(const std::string& s) {
    try {
        size_t pos = 0;
        int value = std::stoi(s, &pos);
        if (pos != s.length()) {
            return Error::make(ErrorCode::InvalidFormat,
                             "trailing characters: " + s.substr(pos));
        }
        return value;
    } catch (const std::exception& e) {
        return Error::make(ErrorCode::InvalidFormat, e.what());
    }
}

void test_parse_int() {
    auto r1 = parse_int("42");
    if (r1) {
        std::cout << "parsed: " << r1.value() << std::endl;
    } else {
        std::cout << "error: " << r1.error().message << std::endl;
    }

    auto r2 = parse_int("abc");
    if (!r2) {
        std::cout << "parse failed: " << r2.error().message << std::endl;
    }
}

但这只是基础,函数式风格的威力在于组合

查看完整可编译示例
// Basic Functional Error Handling
// Demonstrates using std::expected and std::optional for error handling

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

// Error types
enum class ErrorCode {
    FileNotFound,
    InvalidFormat,
    ChecksumMismatch,
    OutOfMemory,
};

struct Error {
    ErrorCode code;
    std::string message;

    static Error make(ErrorCode c, std::string msg) {
        return Error{c, std::move(msg)};
    }
};

// Result type alias
template<typename T>
using Result = std::expected<T, Error>;

using VoidResult = std::expected<void, Error>;

// Example functions
Result<int> parse_int(const std::string& s) {
    try {
        size_t pos = 0;
        int value = std::stoi(s, &pos);
        if (pos != s.length()) {
            return Error::make(ErrorCode::InvalidFormat,
                             "trailing characters: " + s.substr(pos));
        }
        return value;
    } catch (const std::exception& e) {
        return Error::make(ErrorCode::InvalidFormat, e.what());
    }
}

Result<std::string> read_file(const std::string& path) {
    if (path.empty()) {
        return Error::make(ErrorCode::FileNotFound, "empty path");
    }
    return "file content: " + path;
}

VoidResult validate_data(const std::string& data) {
    if (data.empty()) {
        return Error::make(ErrorCode::InvalidFormat, "empty data");
    }
    return {};  // Success
}

void demo_basic_usage() {
    std::cout << "=== Basic Result Usage ===" << std::endl;

    // Success case
    auto r1 = parse_int("42");
    if (r1) {
        std::cout << "Parsed: " << r1.value() << std::endl;
    } else {
        std::cout << "Error: " << r1.error().message << std::endl;
    }

    // Error case
    auto r2 = parse_int("abc");
    if (!r2) {
        std::cout << "Parse failed: " << r2.error().message << std::endl;
    }

    // Value-or-default
    auto r3 = parse_int("invalid");
    int value = r3.value_or(-1);
    std::cout << "Value or default: " << value << std::endl;
}

void demo_void_result() {
    std::cout << "\n=== Void Result ===" << std::endl;

    auto valid = validate_data("some data");
    if (valid) {
        std::cout << "Validation passed" << std::endl;
    }

    auto invalid = validate_data("");
    if (!invalid) {
        std::cout << "Validation failed: " << invalid.error().message << std::endl;
    }
}

void demo_error_propagation() {
    std::cout << "\n=== Error Propagation ===" << std::endl;

    auto process = [](const std::string& path) -> Result<std::string> {
        // Manual error propagation
        auto content_result = read_file(path);
        if (!content_result) {
            return content_result.error();
        }

        auto validation_result = validate_data(content_result.value());
        if (!validation_result) {
            return validation_result.error();
        }

        return content_result.value();
    };

    auto result = process("test.txt");
    if (result) {
        std::cout << "Success: " << result.value() << std::endl;
    } else {
        std::cout << "Failed: " << result.error().message << std::endl;
    }
}

// TRY macro simulation (GCC/Clang statement expression)
#define TRY(...) ({ \
    auto _result = (__VA_ARGS__); \
    if (!_result) return _result.error(); \
    _result.value(); \
})

void demo_try_macro() {
    std::cout << "\n=== TRY Macro ===" << std::endl;

    auto process_clean = [](const std::string& path) -> Result<std::string> {
        auto content = TRY(read_file(path));
        TRY(validate_data(content));
        return content;
    };

    auto result = process_clean("config.txt");
    if (result) {
        std::cout << "Success: " << result.value() << std::endl;
    } else {
        std::cout << "Failed: " << result.error().message << std::endl;
    }
}

int main() {
    demo_basic_usage();
    demo_void_result();
    demo_error_propagation();
    demo_try_macro();

    return 0;
}

组合器模式:and_then与map

and_thenmap是函数式错误处理的两个核心组合器。它们让你能像搭积木一样串联操作。

map:成功时转换值

// map: 如果Result有值,用函数转换它;如果是错误,错误直接穿透
template<typename T, typename F>
auto map(Result<T> result, F&& func) -> Result<decltype(func(result.value()))> {
    if (result) {
        return func(result.value());
    }
    return result.error();
}

// 使用:串联操作
Result<std::string> read_file(const std::string& path);
Result<int> parse_size(const std::string& content);

// 先读文件,再解析大小,错误会自动传播
auto size = map(read_file("config.txt"), parse_size);
// 如果read_file失败,直接返回错误
// 如果成功,把内容传给parse_size

and_then:返回Result的函数链

map的问题是不能处理返回Result的函数。and_then就是为了解决这个问题:

// and_then: 链接返回Result的操作
template<typename T, typename F>
auto and_then(Result<T> result, F&& func)
    -> decltype(func(result.value()))
{
    using ResultType = decltype(func(result.value()));
    if (result) {
        return func(result.value());
    }
    return ResultType(result.error());  // 错误穿透
}

// 使用示例
Result<json::Value> parse_json(const std::string& content);
Result<Config> validate_config(const json::Value& json);
Result<void> apply_config(const Config& cfg);

// 链式调用:错误自动传播,成功时继续执行
Result<void> load_and_apply(const std::string& path) {
    return and_then(
        and_then(
            and_then(
                read_file(path),              // Result<string>
                parse_json                    // Result<json::Value>
            ),
            validate_config                   // Result<Config>
        ),
        apply_config                          // Result<void>
    );
}

看到这个嵌套的and_then了吗?有点丑,所以C++23的std::expected直接提供了成员函数版本:

// C++23风格的链式调用
result.map(transform).and_then(validate).map(finalize);

如果你用的是C++17,可以给expected加上这些方法(参考第8章第5节的实现)。

查看完整可编译示例
// Monadic Combinators for Error Handling
// Demonstrates map, and_then, and other functional combinators

#include <iostream>
#include <expected>
#include <string>
#include <sstream>

struct Error {
    enum Code { ParseError, ValidationError, TransformError };
    Code code;
    std::string message;

    static Error make(Code c, std::string msg) {
        return Error{c, std::move(msg)};
    }
};

template<typename T>
using Result = std::expected<T, Error>;

// map combinator: transform success value
template<typename T, typename F>
auto map(Result<T> result, F&& func) {
    using ResultType = decltype(func(result.value()));
    if constexpr (std::is_void_v<ResultType>) {
        if (result) {
            func(result.value());
            return Result<void>{};
        }
        return Result<void>{std::unexpect, result.error()};
    } else {
        if (result) {
            return Result<ResultType>{func(result.value())};
        }
        return Result<ResultType>{std::unexpect, result.error()};
    }
}

// and_then combinator: chain operations that return Result
template<typename T, typename F>
auto and_then(Result<T> result, F&& func) -> decltype(func(result.value())) {
    if (result) {
        return func(result.value());
    }
    return std::unexpected(result.error());
}

// map_error: transform error
template<typename T, typename F>
Result<T> map_error(Result<T> result, F&& func) {
    if (!result) {
        return Result<T>{std::unexpect, func(result.error())};
    }
    return result;
}

// Demo functions
Result<int> parse_number(const std::string& s) {
    try {
        return Result<int>{std::stoi(s)};
    } catch (...) {
        return Result<int>{std::unexpect, Error::make(Error::ParseError, "Invalid number")};
    }
}

Result<bool> validate_positive(int x) {
    if (x > 0) {
        return true;
    }
    return Result<bool>{std::unexpect, Error::make(Error::ValidationError, "Not positive")};
}

Result<std::string> format_result(int x) {
    std::stringstream ss;
    ss << "Result: " << x;
    return ss.str();
}

void demo_map() {
    std::cout << "=== Map Combinator ===" << std::endl;

    auto result = parse_number("42");
    auto formatted = map(result, format_result);

    if (formatted) {
        std::cout << formatted.value() << std::endl;
    }

    // Chaining map
    auto chained = map(
        map(parse_number("21"), [](int x) { return x * 2; }),
        [](int x) { return x + 10; }
    );

    if (chained) {
        std::cout << "Chained result: " << chained.value() << std::endl;
    }
}

void demo_and_then() {
    std::cout << "\n=== And Then Combinator ===" << std::endl;

    // Chain operations that return Result
    auto result = and_then(
        parse_number("10"),
        validate_positive
    );

    if (result) {
        std::cout << "Valid positive number: " << result.value() << std::endl;
    }

    // Chain with failure
    auto failed = and_then(
        parse_number("-5"),
        validate_positive
    );

    if (!failed) {
        std::cout << "Validation failed: " << failed.error().message << std::endl;
    }

    // Multiple chains
    auto multi_chain = and_then(
        and_then(
            parse_number("5"),
            validate_positive
        ),
        [](bool) { return format_result(42); }
    );

    if (multi_chain) {
        std::cout << "Multi-chain: " << multi_chain.value() << std::endl;
    }
}

void demo_map_error() {
    std::cout << "\n=== Map Error Combinator ===" << std::endl;

    auto result = parse_number("invalid");

    auto enhanced = map_error(result, [](Error err) {
        err.message = "In " + err.message;
        return err;
    });

    if (!enhanced) {
        std::cout << "Enhanced error: " << enhanced.error().message << std::endl;
    }
}

// Composition helper
template<typename T1, typename T2, typename F1, typename F2>
auto compose(F1&& f1, F2&& f2) {
    return [f1, f2](auto&&... args) {
        auto r1 = f1(std::forward<decltype(args)>(args)...);
        if (!r1) {
            using R2 = decltype(f2(r1.value()));
            return R2{std::unexpect, r1.error()};
        }
        return f2(r1.value());
    };
}

void demo_composition() {
    std::cout << "\n=== Function Composition ===" << std::endl;

    auto process = compose(
        parse_number,
        [](int x) { return format_result(x * 2); }
    );

    auto result = process("21");
    if (result) {
        std::cout << "Composed: " << result.value() << std::endl;
    }
}

int main() {
    demo_map();
    demo_and_then();
    demo_map_error();
    demo_composition();

    return 0;
}

实战:配置加载系统

让我们用一个完整的例子展示函数式错误处理的威力。这个系统要从文件加载配置,解析JSON,验证,然后应用。

#include <expected>
#include <string>
#include <fstream>
#include <sstream>

// 错误定义
struct ConfigError {
    enum Code { FileNotFound, ParseError, ValidationError, ApplyError };
    Code code;
    std::string message;

    static ConfigError file_not_found(const std::string& path) {
        return {FileNotFound, "File not found: " + path};
    }

    static ConfigError parse_error(const std::string& detail) {
        return {ParseError, "Parse error: " + detail};
    }

    static ConfigError validation_error(const std::string& field) {
        return {ValidationError, "Validation failed for field: " + field};
    }
};

template<typename T>
using Result = std::expected<T, ConfigError>;

// ========== 各个操作 ==========

// 1. 读文件
Result<std::string> read_file(const std::string& path) {
    std::ifstream f(path);
    if (!f) {
        return ConfigError::file_not_found(path);
    }

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

// 2. 解析JSON(简化版)
struct JsonValue {
    std::string raw;
};

Result<JsonValue> parse_json(const std::string& content) {
    if (content.empty() || content[0] != '{') {
        return ConfigError::parse_error("not a JSON object");
    }
    return JsonValue{content};
}

// 3. 验证配置
struct Config {
    int baudrate = 115200;
    int timeout = 1000;
};

Result<Config> validate_config(const JsonValue& json) {
    Config cfg;
    // 简化:实际会解析JSON内容
    if (json.raw.find("baudrate") == std::string::npos) {
        return ConfigError::validation_error("baudrate");
    }
    return cfg;
}

// 4. 应用配置
Result<void> apply_config(const Config& cfg) {
    // 实际会写入硬件寄存器
    std::cout << "Applied config: baudrate=" << cfg.baudrate
              << ", timeout=" << cfg.timeout << std::endl;
    return {};  // void expected的成功值
}

// ========== 链式组装 ==========

Result<void> load_config(const std::string& path) {
    // 手动版本的and_then链
    auto content_result = read_file(path);
    if (!content_result) return content_result.error();

    auto json_result = parse_json(content_result.value());
    if (!json_result) return json_result.error();

    auto config_result = validate_config(json_result.value());
    if (!config_result) return config_result.error();

    return apply_config(config_result.value());
}

// 或者用我们之前写的and_then简化
Result<void> load_config_chain(const std::string& path) {
    return and_then(
        and_then(
            and_then(
                read_file(path),
                parse_json
            ),
            validate_config
        ),
        apply_config
    );
}

这个例子里,每个操作只关心自己的事情,错误的传播是自动的。你不需要在每个中间步骤写if (error) return error;

查看完整可编译示例
// Configuration Loading with Functional Error Handling
// Demonstrates a complete config loading pipeline using Result types

#include <iostream>
#include <expected>
#include <string>
#include <sstream>
#include <fstream>

// Error types
struct ConfigError {
    enum Code { FileNotFound, ParseError, ValidationError, ApplyError };
    Code code;
    std::string message;

    static ConfigError file_not_found(const std::string& path) {
        return {FileNotFound, "File not found: " + path};
    }

    static ConfigError parse_error(const std::string& detail) {
        return {ParseError, "Parse error: " + detail};
    }

    static ConfigError validation_error(const std::string& field) {
        return {ValidationError, "Validation failed for: " + field};
    }
};

template<typename T>
using Result = std::expected<T, ConfigError>;

// Configuration structure
struct Config {
    int baudrate = 115200;
    int timeout = 1000;
    std::string port = "/dev/ttyUSB0";

    void print() const {
        std::cout << "Config:" << std::endl;
        std::cout << "  baudrate: " << baudrate << std::endl;
        std::cout << "  timeout: " << timeout << std::endl;
        std::cout << "  port: " << port << std::endl;
    }
};

// Step 1: Read file
Result<std::string> read_file(const std::string& path) {
    std::ifstream f(path);
    if (!f) {
        return ConfigError::file_not_found(path);
    }

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

// Step 2: Parse JSON (simplified)
struct JsonValue {
    std::string raw;
};

Result<JsonValue> parse_json(const std::string& content) {
    if (content.empty() || content[0] != '{') {
        return ConfigError::parse_error("not a JSON object");
    }
    return JsonValue{content};
}

// Step 3: Validate and extract config
Result<Config> validate_config(const JsonValue& json) {
    Config cfg;

    // Simplified parsing
    if (json.raw.find("baudrate") == std::string::npos) {
        return ConfigError::validation_error("baudrate");
    }
    if (json.raw.find("timeout") == std::string::npos) {
        return ConfigError::validation_error("timeout");
    }

    // Extract values (simplified)
    cfg.baudrate = 115200;  // Would parse from JSON
    cfg.timeout = 1000;

    return cfg;
}

// Step 4: Apply config
Result<void> apply_config(const Config& cfg) {
    std::cout << "Applying configuration..." << std::endl;
    cfg.print();
    return {};  // Success
}

// TRY macro for clean error propagation
#define TRY(...) ({ \
    auto _result = (__VA_ARGS__); \
    if (!_result) return std::unexpected(_result.error()); \
    _result.value(); \
})

// Load pipeline using manual propagation
Result<void> load_config_manual(const std::string& path) {
    auto content_result = read_file(path);
    if (!content_result) return content_result.error();

    auto json_result = parse_json(content_result.value());
    if (!json_result) return json_result.error();

    auto config_result = validate_config(json_result.value());
    if (!config_result) return config_result.error();

    return apply_config(config_result.value());
}

// Load pipeline using TRY macro
Result<void> load_config_clean(const std::string& path) {
    auto content = TRY(read_file(path));
    auto json = TRY(parse_json(content));
    auto config = TRY(validate_config(json));
    return apply_config(config);
}

// Nested and_then approach
Result<void> load_config_functional(const std::string& path) {
    return read_file(path)
        .and_then([](const std::string& content) {
            return parse_json(content);
        })
        .and_then([](const JsonValue& json) {
            return validate_config(json);
        })
        .and_then([](const Config& cfg) {
            return apply_config(cfg);
        });
}

int main() {
    std::cout << "=== Configuration Loader Demo ===" << std::endl;

    // Test with a mock file content
    std::cout << "\n--- Manual propagation ---" << std::endl;
    auto r1 = load_config_manual("config.json");
    if (!r1) {
        std::cout << "Failed: " << r1.error().message << std::endl;
    }

    std::cout << "\n--- TRY macro ---" << std::endl;
    auto r2 = load_config_clean("config.json");
    if (!r2) {
        std::cout << "Failed: " << r2.error().message << std::endl;
    }

    std::cout << "\n--- Functional (and_then) ---" << std::endl;
    auto r3 = load_config_functional("config.json");
    if (!r3) {
        std::cout << "Failed: " << r3.error().message << std::endl;
    }

    std::cout << "\n=== Key Points ===" << std::endl;
    std::cout << "- Errors are values, not exceptions" << std::endl;
    std::cout << "- Each step returns Result<T, Error>" << std::endl;
    std::cout << "- Error propagation is explicit and type-safe" << std::endl;
    std::cout << "- TRY macro provides clean syntax like Rust's ?" << std::endl;
    std::cout << "- and_then chains operations naturally" << std::endl;

    return 0;
}

宏辅助:简化链式调用

说实话,嵌套的and_then写起来确实有点烦。如果编译器支持C++23,你可以用新的monadic操作;如果暂时用不了,我们可以写个宏:

#define TRY(...) ({ \
    auto _result = (__VA_ARGS__); \
    if (!_result) return _result.error(); \
    _result.value(); \
})

// 使用
Result<void> load_config_clean(const std::string& path) {
    auto content = TRY(read_file(path));
    auto json = TRY(parse_json(content));
    auto config = TRY(validate_config(json));
    return apply_config(config);
}

这个TRY宏在Rust里是内置语法(用?操作符),在C++里我们可以用宏模拟。它的作用是:如果表达式返回错误,直接把错误返回给上层;否则提取值继续执行。

注意:这是GCC/Clang的statement expression语法,MSVC可能需要调整。


错误的转换与增强

有时候底层函数返回的错误不够详细,你需要在传播过程中加上上下文。这可以用map_error实现:

template<typename T, typename F>
Result<T> map_error(Result<T> result, F&& func) {
    if (!result) {
        return func(result.error());
    }
    return result;
}

// 使用:给错误加上上下文
Result<void> load_config_with_context(const std::string& path) {
    return and_then(
        read_file(path),
        [&](const std::string& content) {
            // 在解析失败时加上文件路径上下文
            return map_error(
                parse_json(content),
                [&](ConfigError err) {
                    err.message += " (in file: " + path + ")";
                    return err;
                }
            );
        }
    );
}

这样当解析失败时,错误信息会告诉你哪个文件出问题了,而不是仅仅说"JSON格式错误"。


多个错误的聚合

有时候你不想遇到第一个错误就返回,而是想收集所有错误一起报告:

#include <vector>

template<typename T>
struct AggregateResult {
    std::optional<T> value;
    std::vector<ConfigError> errors;

    bool is_ok() const { return errors.empty(); }
};

// 批量验证:收集所有错误而不是遇到第一个就停
AggregateResult<Config> validate_config_batch(const JsonValue& json) {
    AggregateResult<Config> result;
    Config cfg;
    std::vector<ConfigError> errors;

    // 验证多个字段
    auto check_field = [&](const std::string& name, auto& out) {
        if (json.raw.find(name) == std::string::npos) {
            errors.push_back(ConfigError::validation_error(name));
        }
    };

    check_field("baudrate", cfg.baudrate);
    check_field("timeout", cfg.timeout);
    check_field("address", cfg.address);

    if (errors.empty()) {
        result.value = cfg;
    } else {
        result.errors = std::move(errors);
    }

    return result;
}

这在配置验证、表单验证等场景特别有用——用户可以一次性看到所有问题,而不是修一个错误再运行看下一个。


嵌入式实战:外设初始化链

嵌入式系统里,外设初始化往往是一长串操作,每步都可能失败。函数式错误处理能让这个流程变得清晰:

#include <expected>

// 外设错误类型
struct PeripheralError {
    enum Code {
        ClockNotEnabled,
        GPIOInitFailed,
        PeripheralInitFailed,
        DMAConfigFailed,
    };
    Code code;
    const char* peripheral_name;

    static PeripheralError clock_failed(const char* name) {
        return {ClockNotEnabled, name};
    }

    // ... 其他工厂方法
};

template<typename T>
usingPeriphResult = std::expected<T, PeripheralError>;

// ========== 初始化步骤 ==========

PeriphResult<void> enable_clock(const char* peripheral_name) {
    // 实际会操作RCC寄存器
    std::cout << "Enabling clock for " << peripheral_name << std::endl;
    return {};  // 成功
}

PeriphResult<void> init_gpio(uint8_t pin, const char* mode) {
    std::cout << "Init GPIO " << (int)pin << " as " << mode << std::endl;
    return {};
}

PeriphResult<void> init_uart(uint32_t baudrate) {
    std::cout << "Init UART at " << baudrate << " baud" << std::endl;
    return {};
}

PeriphResult<void> init_dma_channel(uint8_t channel) {
    std::cout << "Init DMA channel " << (int)channel << std::endl;
    return {};
}

// ========== 完整初始化流程 ==========

PeriphResult<void> init_uart_system() {
    const char* uart_name = "USART1";

    // 方式1:手动链式
    auto r1 = enable_clock(uart_name);
    if (!r1) return r1.error();

    auto r2 = init_gpio(9, "alternate");
    if (!r2) return r2.error();

    auto r3 = init_gpio(10, "alternate");
    if (!r3) return r3.error();

    auto r4 = init_uart(115200);
    if (!r4) return r4.error();

    auto r5 = init_dma_channel(4);
    if (!r5) return r5.error();

    return {};

    // 方式2:用TRY宏
    // TRY(enable_clock(uart_name));
    // TRY(init_gpio(9, "alternate"));
    // TRY(init_gpio(10, "alternate"));
    // TRY(init_uart(115200));
    // return init_dma_channel(4);
}

对比传统写法,函数式风格的优势在于:

  • 每一步的错误处理是一致的
  • 不用维护全局的"错误状态变量"
  • 流程是线性的,从上到下读一遍就知道做了什么
查看完整可编译示例
// Peripheral Initialization with Functional Error Handling
// Demonstrates embedded peripheral init chains using Result types

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

// Peripheral error types
struct PeripheralError {
    enum Code {
        ClockNotEnabled,
        GPIOInitFailed,
        PeripheralInitFailed,
        DMAConfigFailed,
        InvalidParameter
    };
    Code code;
    const char* peripheral_name;

    static PeripheralError clock_failed(const char* name) {
        return {ClockNotEnabled, name};
    }

    static PeripheralError init_failed(const char* name) {
        return {PeripheralInitFailed, name};
    }

    const char* to_string() const {
        switch (code) {
            case ClockNotEnabled: return "Clock not enabled";
            case GPIOInitFailed: return "GPIO init failed";
            case PeripheralInitFailed: return "Peripheral init failed";
            case DMAConfigFailed: return "DMA config failed";
            case InvalidParameter: return "Invalid parameter";
        }
        return "Unknown error";
    }
};

template<typename T>
using PeriphResult = std::expected<T, PeripheralError>;

// Specialization for void operations
using VoidPeriphResult = std::expected<void, PeripheralError>;

// Peripheral initialization steps
VoidPeriphResult enable_clock(const char* peripheral_name) {
    std::cout << "  Enabling clock for " << peripheral_name << "..." << std::endl;
    // Simulate success
    return {};
}

VoidPeriphResult init_gpio(uint8_t pin, const char* mode) {
    std::cout << "  Init GPIO " << static_cast<int>(pin) << " as " << mode << "..." << std::endl;
    // Simulate success
    return {};
}

VoidPeriphResult init_uart(uint32_t baudrate) {
    std::cout << "  Init UART at " << baudrate << " baud..." << std::endl;
    // Simulate success
    return {};
}

VoidPeriphResult init_dma_channel(uint8_t channel) {
    std::cout << "  Init DMA channel " << static_cast<int>(channel) << "..." << std::endl;
    // Simulate success
    return {};
}

// TRY macro for clean propagation
#define TRY(...) ({ \
    auto _result = (__VA_ARGS__); \
    if (!_result) return std::unexpected(_result.error()); \
    })

// Complete UART system initialization
VoidPeriphResult init_uart_system() {
    const char* uart_name = "USART1";

    std::cout << "Initializing " << uart_name << "..." << std::endl;

    // Enable clock
    TRY(enable_clock(uart_name));

    // Configure GPIO pins
    TRY(init_gpio(9, "alternate"));
    TRY(init_gpio(10, "alternate"));

    // Initialize UART peripheral
    TRY(init_uart(115200));

    // Setup DMA
    TRY(init_dma_channel(4));

    std::cout << uart_name << " initialized successfully!" << std::endl;
    return {};
}

// SPI initialization
VoidPeriphResult init_spi_system() {
    const char* spi_name = "SPI1";

    std::cout << "Initializing " << spi_name << "..." << std::endl;

    TRY(enable_clock(spi_name));
    TRY(init_gpio(13, "alternate"));  // SCK
    TRY(init_gpio(14, "input"));      // MISO
    TRY(init_gpio(15, "alternate"));  // MOSI

    std::cout << spi_name << " initialized successfully!" << std::endl;
    return {};
}

// Template-based initialization chain
template<typename... Inits>
VoidPeriphResult init_peripheral_chain(const char* name, Inits&&... inits) {
    std::cout << "Initializing " << name << "..." << std::endl;

    (TRY(inits), ...);

    std::cout << name << " initialized successfully!" << std::endl;
    return {};
}

void demo_manual_init() {
    std::cout << "=== Manual Initialization Chain ===" << std::endl;

    auto result = init_uart_system();

    if (!result) {
        std::cout << "Init failed: " << result.error().to_string()
                  << " (" << result.error().peripheral_name << ")" << std::endl;
    }
}

void demo_template_init() {
    std::cout << "\n=== Template-Based Init ===" << std::endl;

    auto result = init_peripheral_chain(
        "SPI1",
        []() { return enable_clock("SPI1"); },
        []() { return init_gpio(13, "alternate"); },
        []() { return init_gpio(14, "input"); },
        []() { return init_gpio(15, "alternate"); }
    );

    if (!result) {
        std::cout << "Init failed: " << result.error().to_string() << std::endl;
    }
}

// Comparison with error code style
void error_code_style_init() {
    std::cout << "\n=== Error Code Style (for comparison) ===" << std::endl;

    int err = 0;

    std::cout << "Initializing USART1..." << std::endl;

    auto r1 = enable_clock("USART1");
    if (!r1) { err = 1; goto error; }

    auto r2 = init_gpio(9, "alternate");
    if (!r2) { err = 2; goto error; }

    auto r3 = init_uart(115200);
    if (!r3) { err = 3; goto error; }

    std::cout << "USART1 initialized!" << std::endl;
    return;

error:
    std::cout << "Init failed with error code " << err << std::endl;
}

void demo_comparison() {
    std::cout << "\n=== Style Comparison ===" << std::endl;
    std::cout << "Error code style requires goto or nested ifs" << std::endl;
    std::cout << "Functional style with TRY macro is linear and readable" << std::endl;
    std::cout << "Both have zero runtime overhead" << std::endl;
}

int main() {
    demo_manual_init();
    demo_template_init();
    error_code_style_init();
    demo_comparison();

    std::cout << "\n=== Benefits ===" << std::endl;
    std::cout << "- Linear code flow (top to bottom)" << std::endl;
    std::cout << "- Error information preserved through chain" << std::endl;
    std::cout << "- Compile-time type safety" << std::endl;
    std::cout << "- Zero overhead compared to error codes" << std::endl;

    return 0;
}

错误恢复与重试策略

函数式错误处理也方便实现重试逻辑:

#include <chrono>
#include <thread>

// 重试包装器
template<typename F, typename Rep, typename Period>
auto retry_with_backoff(F&& func,
                        unsigned max_attempts,
                        std::chrono::duration<Rep, Period> initial_delay)
    -> decltype(func())
{
    using ResultType = decltype(func());

    auto delay = initial_delay;
    for (unsigned attempt = 0; attempt < max_attempts; ++attempt) {
        auto result = func();

        if (result) {
            return result;  // 成功,直接返回
        }

        // 最后一次尝试失败,不再等待
        if (attempt == max_attempts - 1) {
            return result;
        }

        // 等待后重试
        std::this_thread::sleep_for(delay);
        delay *= 2;  // 指数退避
    }

    return ResultType();  // 不应该到这里
}

// 使用:重试可能失败的网络操作
Result<std::string> fetch_http(const std::string& url) {
    // 实际的网络请求...
    if (rand() % 3 == 0) {  // 模拟随机失败
        return ConfigError{ConfigError::FileNotFound, "timeout"};
    }
    return "response body";
}

void test_retry() {
    auto result = retry_with_backoff(
        []() { return fetch_http("http://example.com"); },
        5,                          // 最多5次
        std::chrono::milliseconds(100)  // 初始延迟100ms
    );

    if (result) {
        std::cout << "Got response: " << result.value() << std::endl;
    } else {
        std::cout << "Failed after retries: " << result.error().message << std::endl;
    }
}

与C API的边界处理

嵌入式开发中经常要和C API打交道,它们通常用错误码或返回值表示错误。我们需要在边界处转换:

// C API(假设)
extern "C" {
    typedef int32_t c_status_t;
    #define C_OK 0
    #define C_ERR (-1)

    c_status_t c_hal_init(void);
    c_status_t c_hal_send(const uint8_t* data, size_t len);
}

// C++包装层
struct HalError {
    enum Code { InitFailed, SendFailed, BusError };
    Code code;
    std::string detail;

    static HalError from_c_status(c_status_t status, const char* operation) {
        switch (status) {
            case C_OK: __builtin_unreachable();
            case C_ERR: return {SendFailed, operation};
            default: return {BusError, "unknown error"};
        }
    }
};

template<typename T>
using HalResult = std::expected<T, HalError>;

// 包装C API
HalResult<void> hal_init() {
    c_status_t status = c_hal_init();
    if (status != C_OK) {
        return HalError::from_c_status(status, "init");
    }
    return {};
}

HalResult<void> hal_send(const std::vector<uint8_t>& data) {
    c_status_t status = c_hal_send(data.data(), data.size());
    if (status != C_OK) {
        return HalError::from_c_status(status, "send");
    }
    return {};
}

// 现在可以用函数式风格组织代码
HalResult<void> send_packet(const std::vector<uint8_t>& payload) {
    TRY(hal_init());
    return hal_send(payload);
}

关键是在边界处做一次性转换,然后内部全用函数式风格。这样既保持了与C生态的兼容性,又让C++代码更加清晰。


性能考虑

函数式错误处理的性能开销主要在:

  1. 额外的类型构造expected<T, E>比裸T多存一个错误
  2. 间接调用:组合器可能是lambda,带来额外调用
  3. 代码体积:每个Result<T>是不同类型,增加实例化

实测数据(简化测试):

// 错误码版本
int code_version(int x) {
    if (x < 0) return -1;
    if (x > 100) return -2;
    return x * 2;
}

// Result版本
Result<int> result_version(int x) {
    if (x < 0) return HalError{HalError::BusError, "negative"};
    if (x > 100) return HalError{HalError::BusError, "too large"};
    return x * 2;
}

-O2优化下:

  • 错误码版本:~2条指令(检查+乘法)
  • Result版本:~5条指令(构造expected+检查+乘法+解引用)

性能差距大约2-3倍。但这里的关键是:如果你的函数本身有一定工作量,这个开销可以忽略不计。在IO操作、硬件访问等场景,这点开销完全不可见。

只有在极热路径(比如信号处理、高频中断),才需要考虑用更轻量的错误码。


何时不用函数式错误处理

虽然函数式错误处理很优雅,但也不是万能的:

1. 超热路径

// 1MHz采样率的ADC中断——用错误码
void __attribute__((interrupt)) ADC_IRQHandler() {
    int value = read_adc();
    if (value < 0) { error_count++; return; }  // 简单快速
    process_sample(value);
}

2. 与遗留代码集成

// 如果整个项目都用错误码,你一个人Result反而增加复杂度
int legacy_function() {
    // 调用一堆返回int错误码的函数
}

3. 错误处理不需要上下文

// 简单的true/false就够了
bool try_lock() {
    // 成功返回true,失败返回false
    // 不需要详细错误信息
}

小结

函数式错误处理是一种让代码更优雅、更可维护的方式:

  • 错误是值:类型安全,不能被忽略
  • 可组合:用map/and_then等组合器串联操作
  • 可预测:控制流是线性的,没有异常的突然跳转
  • 上下文丰富:错误信息可以逐层增强

在嵌入式开发中的建议:

场景 推荐方案
应用层初始化 Result/expected
配置解析 Result/expected
IO操作 Result/expected
硬件初始化 Result/expected
高频中断 错误码
极简操作 bool/optional

工具箱里放得下多种工具,但要知道什么时候用什么。函数式错误处理不是银弹,但在很多场景下,它确实比传统方式更清晰、更安全、更易维护。

到这里,我们已经讲完了现代C++中函数式相关的核心工具。Lambda、std::functionstd::invokeoptionalexpected、函数式错误处理——这些组合起来,能让你写出既高效又优雅的代码。嵌入式不代表必须用C风格,现代C++给我们的选择远比想象的多。