嵌入式C++教程——函数式错误处理模式¶
引言¶
写C++的这些年,我见过太多错误的错误处理方式。有人把try-catch当goto用,有人用-1表示所有错误(到底是哪个错误?),有人到处assert然后生产环境直接崩溃。问题的核心不是"要不要处理错误",而是"如何优雅地处理错误"。
函数式编程给了我们一个很好的思路:把错误当成值。不再是异常那种控制流的突然跳转,而是像处理返回值一样处理错误——可以传递、可以转换、可以组合。
我们已经讲过std::optional和std::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_then和map是函数式错误处理的两个核心组合器。它们让你能像搭积木一样串联操作。
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++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++代码更加清晰。
性能考虑¶
函数式错误处理的性能开销主要在:
- 额外的类型构造:
expected<T, E>比裸T多存一个错误 - 间接调用:组合器可能是lambda,带来额外调用
- 代码体积:每个
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. 与遗留代码集成¶
3. 错误处理不需要上下文¶
小结¶
函数式错误处理是一种让代码更优雅、更可维护的方式:
- 错误是值:类型安全,不能被忽略
- 可组合:用
map/and_then等组合器串联操作 - 可预测:控制流是线性的,没有异常的突然跳转
- 上下文丰富:错误信息可以逐层增强
在嵌入式开发中的建议:
| 场景 | 推荐方案 |
|---|---|
| 应用层初始化 | Result/expected |
| 配置解析 | Result/expected |
| IO操作 | Result/expected |
| 硬件初始化 | Result/expected |
| 高频中断 | 错误码 |
| 极简操作 | bool/optional |
工具箱里放得下多种工具,但要知道什么时候用什么。函数式错误处理不是银弹,但在很多场景下,它确实比传统方式更清晰、更安全、更易维护。
到这里,我们已经讲完了现代C++中函数式相关的核心工具。Lambda、std::function、std::invoke、optional、expected、函数式错误处理——这些组合起来,能让你写出既高效又优雅的代码。嵌入式不代表必须用C风格,现代C++给我们的选择远比想象的多。