009 《Folly Expected.h 权威指南》
🌟🌟🌟本文案由Gemini 2.0 Flash Thinking Experimental 01-21创作,用来辅助学习知识。🌟🌟🌟
书籍大纲
▮▮▮▮ 1. chapter 1:Expected.h 概览 (Overview)
▮▮▮▮▮▮▮ 1.1 为什么选择 Expected (Why Expected?)
▮▮▮▮▮▮▮▮▮▮▮ 1.1.1 错误处理的困境 (The Dilemma of Error Handling)
▮▮▮▮▮▮▮▮▮▮▮ 1.1.2 Expected 的设计哲学 (Design Philosophy of Expected)
▮▮▮▮▮▮▮ 1.2 Expected.h 能为你做什么 (What can Expected.h do for you?)
▮▮▮▮▮▮▮▮▮▮▮ 1.2.1 清晰地表达结果:值或错误 (Clearly Express Results: Value or Error)
▮▮▮▮▮▮▮▮▮▮▮ 1.2.2 链式操作与函数式编程 (Chaining Operations and Functional Programming)
▮▮▮▮▮▮▮▮▮▮▮ 1.2.3 提升代码可读性和可维护性 (Improve Code Readability and Maintainability)
▮▮▮▮▮▮▮ 1.3 Expected 与其他错误处理方式的对比 (Comparison with Other Error Handling Methods)
▮▮▮▮▮▮▮▮▮▮▮ 1.3.1 与异常 (Exceptions) 的对比
▮▮▮▮▮▮▮▮▮▮▮ 1.3.2 与错误码 (Error Codes) 的对比
▮▮▮▮▮▮▮▮▮▮▮ 1.3.3 与 std::optional 的对比
▮▮▮▮▮▮▮ 1.4 快速上手:你的第一个 Expected (Quick Start: Your First Expected)
▮▮▮▮▮▮▮▮▮▮▮ 1.4.1 包含头文件 (Include Header File)
▮▮▮▮▮▮▮▮▮▮▮ 1.4.2 创建 Expected 对象 (Creating Expected Objects)
▮▮▮▮▮▮▮▮▮▮▮ 1.4.3 检查 Expected 的状态 (Checking Expected Status)
▮▮▮▮▮▮▮▮▮▮▮ 1.4.4 访问 Expected 的值或错误 (Accessing Expected Value or Error)
▮▮▮▮ 2. chapter 2:Expected 的基本操作 (Basic Operations of Expected)
▮▮▮▮▮▮▮ 2.1 构造与赋值 (Construction and Assignment)
▮▮▮▮▮▮▮▮▮▮▮ 2.1.1 默认构造函数 (Default Constructor)
▮▮▮▮▮▮▮▮▮▮▮ 2.1.2 值构造函数 (Value Constructor)
▮▮▮▮▮▮▮▮▮▮▮ 2.1.3 错误构造函数 (Error Constructor)
▮▮▮▮▮▮▮▮▮▮▮ 2.1.4 移动构造与移动赋值 (Move Construction and Move Assignment)
▮▮▮▮▮▮▮▮▮▮▮ 2.1.5 拷贝构造与拷贝赋值 (Copy Construction and Copy Assignment)
▮▮▮▮▮▮▮ 2.2 状态检查 (Status Checking)
▮▮▮▮▮▮▮▮▮▮▮ 2.2.1 hasValue():检查是否包含值 (Check if Contains Value)
▮▮▮▮▮▮▮▮▮▮▮ 2.2.2 hasError():检查是否包含错误 (Check if Contains Error)
▮▮▮▮▮▮▮ 2.3 值与错误的访问 (Accessing Value and Error)
▮▮▮▮▮▮▮▮▮▮▮ 2.3.1 value():访问值 (Access Value)
▮▮▮▮▮▮▮▮▮▮▮ 2.3.2 error():访问错误 (Access Error)
▮▮▮▮▮▮▮▮▮▮▮ 2.3.3 valueOr():提供默认值 (Provide Default Value)
▮▮▮▮▮▮▮▮▮▮▮ 2.3.4 errorOr():提供默认错误 (Provide Default Error)
▮▮▮▮▮▮▮ 2.4 Expected 的转换 (Conversion of Expected)
▮▮▮▮▮▮▮▮▮▮▮ 2.4.1 转换为 bool 类型 (Conversion to bool Type)
▮▮▮▮▮▮▮▮▮▮▮ 2.4.2 与 std::optional 的互操作 (Interoperation with std::optional)
▮▮▮▮ 3. chapter 3:Expected 的函数式操作 (Functional Operations of Expected)
▮▮▮▮▮▮▮ 3.1 map():转换值 (Transform Value)
▮▮▮▮▮▮▮▮▮▮▮ 3.1.1 map() 的基本用法 (Basic Usage of map())
▮▮▮▮▮▮▮▮▮▮▮ 3.1.2 map() 的链式调用 (Chaining map())
▮▮▮▮▮▮▮ 3.2 mapError():转换错误 (Transform Error)
▮▮▮▮▮▮▮▮▮▮▮ 3.2.1 mapError() 的基本用法 (Basic Usage of mapError())
▮▮▮▮▮▮▮▮▮▮▮ 3.2.2 mapError() 的应用场景 (Application Scenarios of mapError())
▮▮▮▮▮▮▮ 3.3 andThen() (flatMap):链式操作与结果组合 (Chaining Operations and Combining Results)
▮▮▮▮▮▮▮▮▮▮▮ 3.3.1 andThen() 的基本用法 (Basic Usage of andThen())
▮▮▮▮▮▮▮▮▮▮▮ 3.3.2 andThen() 的 Monadic 特性 (Monadic Properties of andThen())
▮▮▮▮▮▮▮ 3.4 orElse():处理错误情况 (Handling Error Cases)
▮▮▮▮▮▮▮▮▮▮▮ 3.4.1 orElse() 的基本用法 (Basic Usage of orElse())
▮▮▮▮▮▮▮▮▮▮▮ 3.4.2 orElse() 的错误恢复 (Error Recovery with orElse())
▮▮▮▮▮▮▮ 3.5 transform():通用转换 (General Transformation)
▮▮▮▮▮▮▮▮▮▮▮ 3.5.1 transform() 的灵活性 (Flexibility of transform())
▮▮▮▮▮▮▮▮▮▮▮ 3.5.2 transform() 的应用示例 (Examples of transform())
▮▮▮▮ 4. chapter 4:高级应用与实战案例 (Advanced Applications and Practical Cases)
▮▮▮▮▮▮▮ 4.1 自定义错误类型 (Custom Error Types)
▮▮▮▮▮▮▮▮▮▮▮ 4.1.1 使用枚举 (Enums) 定义错误类型
▮▮▮▮▮▮▮▮▮▮▮ 4.1.2 使用类 (Classes) 定义错误类型
▮▮▮▮▮▮▮▮▮▮▮ 4.1.3 错误类型的继承与多态 (Inheritance and Polymorphism of Error Types)
▮▮▮▮▮▮▮ 4.2 Expected 与资源管理 (Resource Management)
▮▮▮▮▮▮▮▮▮▮▮ 4.2.1 RAII 与 Expected (RAII and Expected)
▮▮▮▮▮▮▮▮▮▮▮ 4.2.2 在 Expected 中管理资源生命周期 (Managing Resource Lifecycles in Expected)
▮▮▮▮▮▮▮ 4.3 Expected 在并发编程中的应用 (Expected in Concurrent Programming)
▮▮▮▮▮▮▮▮▮▮▮ 4.3.1 线程安全 (Thread Safety)
▮▮▮▮▮▮▮▮▮▮▮ 4.3.2 异步操作与 Expected (Asynchronous Operations and Expected)
▮▮▮▮▮▮▮ 4.4 性能考量与优化 (Performance Considerations and Optimization)
▮▮▮▮▮▮▮▮▮▮▮ 4.4.1 Expected 的性能开销 (Performance Overhead of Expected)
▮▮▮▮▮▮▮▮▮▮▮ 4.4.2 避免不必要的拷贝 (Avoiding Unnecessary Copies)
▮▮▮▮▮▮▮▮▮▮▮ 4.4.3 选择合适的错误类型 (Choosing Appropriate Error Types)
▮▮▮▮▮▮▮ 4.5 实战案例:构建健壮的 API (Practical Case: Building Robust APIs)
▮▮▮▮▮▮▮▮▮▮▮ 4.5.1 使用 Expected 设计 API 接口 (Designing API Interfaces with Expected)
▮▮▮▮▮▮▮▮▮▮▮ 4.5.2 错误码与错误信息的设计 (Design of Error Codes and Error Messages)
▮▮▮▮▮▮▮▮▮▮▮ 4.5.3 API 版本的兼容性 (API Version Compatibility)
▮▮▮▮ 5. chapter 5:Expected API 全面解析 (Comprehensive API Analysis)
▮▮▮▮▮▮▮ 5.1 Expected 类模板详解 (Detailed Explanation of Expected Class Template)
▮▮▮▮▮▮▮▮▮▮▮ 5.1.1 模板参数 (Template Parameters)
▮▮▮▮▮▮▮▮▮▮▮ 5.1.2 成员类型 (Member Types)
▮▮▮▮▮▮▮ 5.2 构造函数与析构函数 (Constructors and Destructor)
▮▮▮▮▮▮▮▮▮▮▮ 5.2.1 各种构造函数的详细说明 (Detailed Description of Various Constructors)
▮▮▮▮▮▮▮▮▮▮▮ 5.2.2 析构函数的行为 (Behavior of Destructor)
▮▮▮▮▮▮▮ 5.3 成员函数详解 (Detailed Explanation of Member Functions)
▮▮▮▮▮▮▮▮▮▮▮ 5.3.1 状态检查函数 (Status Checking Functions)
▮▮▮▮▮▮▮▮▮▮▮ 5.3.2 值与错误访问函数 (Value and Error Access Functions)
▮▮▮▮▮▮▮▮▮▮▮ 5.3.3 转换操作函数 (Transformation Operation Functions)
▮▮▮▮▮▮▮▮▮▮▮ 5.3.4 其他实用函数 (Other Utility Functions)
▮▮▮▮▮▮▮ 5.4 非成员函数 (Non-member Functions)
▮▮▮▮▮▮▮▮▮▮▮ 5.4.1 比较运算符 (Comparison Operators)
▮▮▮▮▮▮▮▮▮▮▮ 5.4.2 交换函数 (Swap Function)
▮▮▮▮ 6. chapter 6:Expected 的最佳实践与陷阱 (Best Practices and Pitfalls of Expected)
▮▮▮▮▮▮▮ 6.1 何时使用 Expected (When to Use Expected)
▮▮▮▮▮▮▮▮▮▮▮ 6.1.1 函数可能失败但非致命错误 (Functions May Fail but Non-Fatal Errors)
▮▮▮▮▮▮▮▮▮▮▮ 6.1.2 需要清晰区分正常结果与错误 (Need to Clearly Distinguish Normal Results from Errors)
▮▮▮▮▮▮▮ 6.2 避免过度使用 Expected (Avoiding Overuse of Expected)
▮▮▮▮▮▮▮▮▮▮▮ 6.2.1 简单错误处理场景 (Simple Error Handling Scenarios)
▮▮▮▮▮▮▮▮▮▮▮ 6.2.2 性能敏感型代码 (Performance-Sensitive Code)
▮▮▮▮▮▮▮ 6.3 常见的陷阱与错误用法 (Common Pitfalls and Misuses)
▮▮▮▮▮▮▮▮▮▮▮ 6.3.1 忽略错误 (Ignoring Errors)
▮▮▮▮▮▮▮▮▮▮▮ 6.3.2 不恰当的错误类型 (Inappropriate Error Types)
▮▮▮▮▮▮▮▮▮▮▮ 6.3.3 过度嵌套的 Expected (Excessively Nested Expected)
▮▮▮▮▮▮▮ 6.4 与其他 Folly 库的集成 (Integration with Other Folly Libraries)
▮▮▮▮▮▮▮▮▮▮▮ 6.4.1 与 Futures 的结合使用 (Using with Futures)
▮▮▮▮▮▮▮▮▮▮▮ 6.4.2 与其他 Folly 组件的协同工作 (Working with Other Folly Components)
▮▮▮▮ 7. chapter 7:总结与展望 (Summary and Outlook)
▮▮▮▮▮▮▮ 7.1 Expected 的优势与局限性总结 (Summary of Advantages and Limitations of Expected)
▮▮▮▮▮▮▮ 7.2 Expected 的未来发展趋势 (Future Development Trends of Expected)
▮▮▮▮▮▮▮ 7.3 结语:构建更健壮的 C++ 代码 (Conclusion: Building More Robust C++ Code)
1. chapter 1:Expected.h 概览 (Overview)
1.1 为什么选择 Expected (Why Expected?)
1.1.1 错误处理的困境 (The Dilemma of Error Handling)
在软件开发中,错误处理是一项至关重要的任务。一个健壮的程序不仅需要能够完成预期的功能,更需要在面对各种异常情况时,能够优雅地处理错误,避免程序崩溃,并提供有用的错误信息。然而,传统的 C++ 错误处理机制,如异常(Exceptions)和错误码(Error Codes),在某些场景下存在固有的局限性,使得错误处理成为一个长期困扰开发者的问题。
① 异常的不可预测性与性能开销:
异常机制在 C++ 中被广泛用于错误处理。当函数遇到无法正常处理的错误时,可以抛出一个异常,程序的控制流会跳转到最近的 catch
块进行处理。虽然异常提供了一种强大的错误处理机制,但其使用也存在一些问题:
⚝ 控制流的隐式跳转:异常的抛出和捕获会导致程序控制流的非局部跳转,这使得代码的执行路径变得难以预测和跟踪。尤其是在大型项目中,异常的滥用会降低代码的可读性和可维护性。
⚝ 性能开销:异常处理在某些情况下会带来性能开销。即使没有异常抛出,编译器也需要生成额外的代码来支持异常处理机制,这可能会对程序的性能产生轻微的影响。更重要的是,当异常真正抛出时,堆栈展开(stack unwinding)的过程可能会非常耗时,尤其是在异常频繁发生的场景下。
⚝ 异常安全性的挑战:编写异常安全的代码需要仔细考虑资源管理和状态一致性。如果异常在不恰当的时机抛出,可能会导致资源泄漏或数据损坏。保证代码的异常安全性需要额外的开发和测试成本。
② 错误码的冗余与易错性:
另一种常见的错误处理方式是使用错误码。函数通过返回特定的错误码来指示操作是否成功,调用者需要检查返回值来判断是否发生了错误。错误码机制虽然简单直接,但也存在一些不足:
⚝ 返回值歧义:当函数需要返回一个有效值时,就难以同时使用返回值来传递错误信息。通常的做法是使用特殊的返回值(如 -1
,nullptr
)或输出参数来传递错误码。这容易导致返回值含义的混淆,降低代码的可读性。
⚝ 错误处理的遗漏:由于错误码是可选的返回值,开发者可能会忘记检查错误码,从而导致错误被忽略。尤其是在复杂的函数调用链中,错误码的传递和检查容易被遗漏,使得错误处理变得不可靠。
⚝ 缺乏类型安全:错误码通常使用整数类型表示,缺乏类型安全性。不同的错误码可能具有相同的类型,编译器无法帮助开发者区分不同类型的错误,容易导致错误处理逻辑的混乱。
③ std::optional
的局限性:
C++17 引入了 std::optional
,用于表示可能存在或不存在的值。std::optional
可以用来处理函数可能返回空值的情况,但它主要关注的是值的缺失,而不是错误的处理。std::optional
只能表示“有值”或“无值”两种状态,无法提供关于错误类型和错误信息的详细描述。因此,std::optional
并不适用于需要明确区分正常结果和错误,并需要传递错误信息的错误处理场景。
面对这些错误处理的困境,我们需要一种更优雅、更安全、更高效的错误处理方案。folly::Expected
正是为了解决这些问题而设计的。它旨在提供一种类型安全的、显式的、函数式的错误处理机制,帮助开发者编写更健壮、更可读、更易维护的 C++ 代码。
1.1.2 Expected 的设计哲学 (Design Philosophy of Expected)
folly::Expected
的设计哲学根植于对现代 C++ 错误处理最佳实践的深刻理解和反思。它旨在克服传统错误处理方法(如异常和错误码)的局限性,并借鉴函数式编程的思想,提供一种更清晰、更安全、更高效的错误处理方案。其核心设计哲学可以概括为以下几个方面:
① 显式性与类型安全 (Explicitness and Type Safety):
Expected
强制开发者显式地处理可能发生的错误。与异常的隐式控制流不同,Expected
将错误处理纳入函数的返回类型中,使得错误处理成为函数接口的一部分。通过 Expected<T, E>
模板类,明确地表示函数可能返回类型为 T
的值,或者类型为 E
的错误。这种类型安全的设计,使得编译器可以在编译时检查错误处理的正确性,避免因类型不匹配导致的错误。
② 值语义与无异常 (Value Semantics and No Exceptions):
Expected
采用值语义,其行为类似于一个容器,可以容纳一个值或者一个错误。与异常不同,Expected
本身不涉及控制流的跳转,错误处理完全在函数调用者的上下文中进行。这种基于值的设计,避免了异常的性能开销和控制流复杂性。同时,Expected
的设计目标是在不使用异常的情况下实现错误处理,这使得它在一些禁止或不鼓励使用异常的环境中(如性能敏感的代码、嵌入式系统)也能够发挥作用。
③ 函数式编程范式 (Functional Programming Paradigm):
Expected
借鉴了函数式编程中 Result
或 Either
类型的概念,提供了 map
、mapError
、andThen
(flatMap)、orElse
等函数式操作。这些操作符允许开发者以链式调用的方式处理 Expected
对象,将错误处理逻辑与业务逻辑清晰地分离。函数式操作符不仅提高了代码的可读性和简洁性,也使得错误处理更加灵活和强大。例如,可以使用 map
操作符在值存在时对其进行转换,使用 mapError
操作符在错误发生时对其进行转换,使用 andThen
操作符进行链式操作,并在前一步操作成功时执行下一步操作,使用 orElse
操作符在错误发生时提供备选方案或进行错误恢复。
④ 组合性与可扩展性 (Composability and Extensibility):
Expected
被设计成易于组合和扩展的。它可以与其他 Expected
对象组合,形成复杂的错误处理流程。同时,Expected
可以与其他的库和框架(如 folly::Future
)无缝集成,构建更强大的异步和并发编程模型。开发者可以根据自己的需求,自定义错误类型,扩展 Expected
的功能,使其更好地适应不同的应用场景。
⑤ 性能与效率 (Performance and Efficiency):
Expected
在设计时充分考虑了性能和效率。它避免了异常的性能开销,采用了轻量级的实现方式。通过移动语义和就地构造等技术,减少了不必要的拷贝和内存分配。在大多数情况下,Expected
的性能开销可以忽略不计,甚至在某些场景下,使用 Expected
进行错误处理可能比使用异常更高效。
总而言之,folly::Expected
的设计哲学是追求显式、安全、高效、函数式的错误处理。它试图在传统的错误处理方法和现代编程范式之间找到一个平衡点,为 C++ 开发者提供一种更优秀的错误处理工具。
1.2 Expected.h 能为你做什么 (What can Expected.h do for you?)
folly::Expected
不仅仅是一个简单的错误处理工具,它是一套全面的解决方案,旨在提升 C++ 代码的质量、可读性和可维护性。通过采用 Expected
,开发者可以更加清晰地表达函数的返回结果,更加优雅地处理错误,并充分利用函数式编程的优势。下面我们将详细介绍 Expected
能够为我们做什么。
1.2.1 清晰地表达结果:值或错误 (Clearly Express Results: Value or Error)
Expected
最核心的功能是清晰地表达函数的返回结果。传统的函数返回值要么只返回正常结果,要么使用特殊的返回值或输出参数来指示错误,这容易导致返回值含义的混淆。而 Expected<T, E>
类型则明确地表示一个函数可能返回两种结果之一:
① 成功返回值 (Value):当函数执行成功时,Expected
对象会包含一个类型为 T
的值,表示函数的正常结果。
② 错误返回值 (Error):当函数执行失败时,Expected
对象会包含一个类型为 E
的错误,表示函数执行过程中发生的错误信息。
这种设计使得函数的返回类型本身就包含了错误处理的信息,调用者可以清晰地知道函数可能返回的结果类型,并据此进行相应的处理。例如,考虑一个文件读取函数 readFile
,使用 Expected
可以这样定义其返回类型:
1
#include <folly/Expected.h>
2
#include <string>
3
#include <fstream>
4
#include <system_error>
5
6
using namespace folly;
7
8
Expected<std::string, std::error_code> readFile(const std::string& filename) {
9
std::ifstream file(filename);
10
if (!file.is_open()) {
11
return makeUnexpected(std::error_code(errno, std::generic_category()));
12
}
13
std::string content((std::istreambuf_iterator<char>(file)),
14
std::istreambuf_iterator<char>());
15
return content;
16
}
在这个例子中,readFile
函数的返回类型是 Expected<std::string, std::error_code>
。这意味着函数要么成功返回一个 std::string
类型的文件内容,要么返回一个 std::error_code
类型的错误,表示文件读取失败的原因。调用者可以根据 Expected
对象的状态,判断操作是否成功,并访问相应的结果或错误信息。
1
int main() {
2
auto result = readFile("example.txt");
3
if (result.hasValue()) {
4
std::string content = result.value();
5
// 处理文件内容
6
std::cout << "File content:\n" << content << std::endl;
7
} else {
8
std::error_code error = result.error();
9
// 处理错误
10
std::cerr << "Error reading file: " << error.message() << std::endl;
11
}
12
return 0;
13
}
通过 Expected
,函数的返回类型清晰地表达了可能的结果,避免了返回值含义的歧义,提高了代码的可读性和可理解性。
1.2.2 链式操作与函数式编程 (Chaining Operations and Functional Programming)
Expected
提供了丰富的函数式操作符,如 map
、mapError
、andThen
、orElse
、transform
等,使得开发者可以使用链式调用的方式处理 Expected
对象,实现复杂的错误处理逻辑。这些函数式操作符不仅简化了代码,也使得错误处理更加灵活和强大。
① map()
操作:
map()
操作符用于在 Expected
对象包含值时,对值进行转换。如果 Expected
对象包含错误,则 map()
操作不会执行,直接返回包含相同错误的 Expected
对象。这使得我们可以在链式调用中,只处理成功的情况,而忽略错误的情况,直到最后统一处理错误。
② mapError()
操作:
mapError()
操作符与 map()
类似,但它用于在 Expected
对象包含错误时,对错误进行转换。如果 Expected
对象包含值,则 mapError()
操作不会执行,直接返回包含相同值的 Expected
对象。这使得我们可以方便地转换错误类型或错误信息。
③ andThen()
(flatMap) 操作:
andThen()
操作符用于将多个返回 Expected
对象的函数串联起来。它接受一个返回 Expected
对象的函数作为参数,并在前一个 Expected
对象包含值时,将该值传递给参数函数,执行下一步操作。如果前一个 Expected
对象包含错误,则 andThen()
操作不会执行,直接返回包含相同错误的 Expected
对象。andThen()
操作符是实现 Monadic 链式调用的关键,它可以将多个可能失败的操作组合成一个复杂的流程,并在任何一个环节发生错误时,中断流程并返回错误。
④ orElse()
操作:
orElse()
操作符用于处理错误情况。它接受一个返回 Expected
对象的函数或一个 Expected
对象作为参数。当 Expected
对象包含错误时,orElse()
操作会执行参数函数或返回参数 Expected
对象,提供一个备选方案或进行错误恢复。如果 Expected
对象包含值,则 orElse()
操作不会执行,直接返回原始 Expected
对象。
通过这些函数式操作符,我们可以构建优雅的链式调用,将错误处理逻辑融入到业务流程中,使得代码更加简洁、易读、易维护。例如,假设我们需要从配置文件中读取配置项,并根据配置项的值进行不同的处理,可以使用 Expected
和函数式操作符来实现:
1
#include <folly/Expected.h>
2
#include <string>
3
#include <fstream>
4
#include <system_error>
5
#include <sstream>
6
7
using namespace folly;
8
9
Expected<std::string, std::error_code> readConfig(const std::string& filename) { /* ... 读取配置文件 ... */ }
10
Expected<int, std::error_code> parseConfigValue(const std::string& config) { /* ... 解析配置值 ... */ }
11
Expected<void, std::error_code> applyConfig(int value) { /* ... 应用配置 ... */ }
12
13
int main() {
14
readConfig("config.ini")
15
.andThen(parseConfigValue)
16
.andThen(applyConfig)
17
.orElse([](const Expected<void, std::error_code>& errorExp) {
18
std::cerr << "Configuration failed: " << errorExp.error().message() << std::endl;
19
return errorExp; // or return Expected<void, std::error_code>(unexpect, errorExp.error());
20
});
21
return 0;
22
}
在这个例子中,我们使用链式调用将读取配置、解析配置值、应用配置三个操作串联起来。如果任何一个操作失败,整个流程会中断,并最终调用 orElse
提供的错误处理函数。这种链式调用方式使得代码逻辑清晰,错误处理集中,提高了代码的可读性和可维护性。
1.2.3 提升代码可读性和可维护性 (Improve Code Readability and Maintainability)
Expected
通过其显式的返回类型、函数式操作符和类型安全的设计,显著提升了代码的可读性和可维护性。
① 更清晰的函数接口:
Expected
使得函数的返回类型能够清晰地表达函数可能返回的结果类型,包括正常值和错误信息。这使得函数接口更加自文档化,调用者可以一目了然地了解函数可能的结果,并据此进行相应的处理。
② 更简洁的错误处理代码:
通过函数式操作符,Expected
允许开发者使用链式调用的方式处理错误,避免了大量的 if-else
嵌套和错误码检查代码。这使得错误处理代码更加简洁、清晰,降低了代码的复杂性。
③ 更强的类型安全性:
Expected
是一个模板类,它对值类型和错误类型都进行了类型约束。编译器可以在编译时检查 Expected
对象的使用是否符合类型安全规则,避免因类型不匹配导致的错误。这提高了代码的健壮性和可靠性。
④ 更好的代码组织:
Expected
鼓励将错误处理逻辑与业务逻辑分离。通过函数式操作符,可以将错误处理逻辑集中在链式调用的末端或使用 orElse
进行统一处理,使得业务逻辑代码更加专注于核心功能,提高了代码的可读性和可维护性。
⑤ 易于测试:
使用 Expected
的代码更容易进行单元测试。可以针对函数返回成功值和错误值两种情况编写测试用例,全面覆盖函数的行为。函数式操作符也使得测试代码更加简洁和易于组合。
总而言之,folly::Expected
通过其独特的设计理念和功能特性,为 C++ 开发者提供了一种更优秀的错误处理方案,帮助开发者编写更易读、更易维护、更健壮的代码。
1.3 Expected 与其他错误处理方式的对比 (Comparison with Other Error Handling Methods)
为了更好地理解 Expected
的优势和适用场景,我们需要将其与其他常见的 C++ 错误处理方式进行对比,包括异常(Exceptions)、错误码(Error Codes)和 std::optional
。
1.3.1 与异常 (Exceptions) 的对比
异常和 Expected
是两种截然不同的错误处理机制,它们在设计理念、使用方式和适用场景上存在显著差异。
特性 | 异常 (Exceptions) | Expected |
---|---|---|
设计理念 | 信号机制,表示程序运行时发生的异常情况,控制流跳转 | 值类型,表示函数可能返回值或错误,显式返回错误 |
控制流 | 隐式控制流跳转 (非局部跳转) | 显式控制流 (基于值) |
错误处理 | 集中式错误处理 (try-catch 块) | 分布式错误处理 (函数式操作符) |
性能开销 | 可能存在性能开销 (堆栈展开) | 性能开销较小 (基于值语义) |
类型安全 | 错误类型可以是任意类型 (但通常使用继承体系) | 类型安全 (通过模板参数指定值类型和错误类型) |
显式性 | 隐式错误处理 (函数签名不显式声明可能抛出异常) | 显式错误处理 (函数签名明确声明可能返回错误) |
适用场景 | 异常情况、不可恢复的错误、错误处理逻辑复杂 | 可预测的错误、可恢复的错误、函数式编程、需要高性能 |
代码可读性 | 异常滥用可能降低代码可读性 | 函数式操作符提高代码可读性 |
代码维护性 | 异常处理逻辑分散,维护性较差 | 错误处理逻辑集中,维护性较好 |
① 设计理念与控制流:
异常是一种信号机制,用于表示程序运行时发生的异常情况,例如内存不足、文件不存在等。当异常抛出时,程序的控制流会隐式地跳转到最近的 catch
块进行处理。这种非局部跳转使得代码的执行路径变得难以预测。
Expected
则是一种值类型,用于表示函数可能返回值或错误。它将错误处理纳入函数的返回类型中,使得错误处理成为函数接口的一部分。错误处理的控制流是显式的,基于值的传递和函数式操作符的链式调用。
② 错误处理方式:
异常采用集中式错误处理,通过 try-catch
块来捕获和处理异常。错误处理逻辑通常集中在 catch
块中。
Expected
采用分布式错误处理,通过函数式操作符(如 map
、andThen
、orElse
)在链式调用中处理错误。错误处理逻辑分散在各个函数式操作符中,但可以通过链式调用进行组合和管理。
③ 性能开销:
异常处理在某些情况下可能存在性能开销,尤其是在异常频繁抛出的场景下,堆栈展开的过程可能会比较耗时。即使没有异常抛出,编译器也需要生成额外的代码来支持异常处理机制。
Expected
的性能开销相对较小,因为它基于值语义,避免了控制流的跳转和堆栈展开。在大多数情况下,Expected
的性能开销可以忽略不计。
④ 类型安全与显式性:
异常的错误类型可以是任意类型,但通常建议使用继承自 std::exception
的类来表示错误类型。异常的类型安全性主要依赖于良好的异常类设计和 catch
块的类型匹配。函数签名通常不显式声明可能抛出的异常,这使得异常处理具有一定的隐式性。
Expected
是类型安全的,通过模板参数 Expected<T, E>
明确指定了值类型 T
和错误类型 E
。函数签名明确声明了可能返回错误,使得错误处理更加显式。
⑤ 适用场景:
异常适用于处理异常情况、不可恢复的错误、错误处理逻辑复杂的场景。例如,当程序遇到严重的错误,无法继续正常执行时,可以抛出异常,终止程序或进行全局错误处理。
Expected
适用于处理可预测的错误、可恢复的错误、需要函数式编程、需要高性能的场景。例如,当函数可能因为输入错误、资源不可用等原因失败时,可以使用 Expected
返回错误信息,由调用者决定如何处理错误。
⑥ 代码可读性与维护性:
异常的滥用可能导致代码可读性降低,因为异常的控制流是隐式的,错误处理逻辑分散在各个 catch
块中。
Expected
通过函数式操作符和链式调用,提高了代码的可读性。错误处理逻辑集中在链式调用的末端或使用 orElse
进行统一处理,使得代码结构更清晰,维护性更好。
总结:
异常和 Expected
各有优缺点,适用于不同的场景。异常适用于处理异常情况和不可恢复的错误,而 Expected
更适用于处理可预测的错误和需要函数式编程的场景。在实际开发中,可以根据具体的应用场景和需求,选择合适的错误处理机制,或者将两者结合使用。例如,可以使用异常处理程序级别的、不可预期的错误,而使用 Expected
处理函数级别的、可预期的错误。
1.3.2 与错误码 (Error Codes) 的对比
错误码是另一种常见的错误处理方式,它通过函数的返回值或输出参数来传递错误信息。Expected
与错误码相比,在类型安全、可读性和函数式编程方面具有明显的优势。
特性 | 错误码 (Error Codes) | Expected |
---|---|---|
错误表示 | 特殊返回值 (如 -1, nullptr) 或输出参数传递错误码 | Expected<T, E> 类型明确表示值或错误 |
类型安全 | 错误码通常使用整数类型表示,缺乏类型安全 | 类型安全 (通过模板参数指定错误类型) |
显式性 | 需要手动检查错误码,容易遗漏错误处理 | 显式错误处理 (函数签名明确声明可能返回错误) |
可读性 | 错误码含义需要查阅文档,可读性较差 | Expected 类型和函数式操作符提高代码可读性 |
函数式编程 | 难以进行函数式编程 | 提供丰富的函数式操作符,易于函数式编程 |
错误信息 | 错误码通常只包含错误代码,缺乏详细错误信息 | 错误类型可以是任意类型,可以包含详细错误信息 |
代码维护性 | 错误码检查代码分散,维护性较差 | 错误处理逻辑集中,维护性较好 |
① 错误表示与类型安全:
错误码通常使用特殊的返回值(如 -1
,nullptr
)或输出参数来传递错误信息。这种方式容易导致返回值含义的混淆,且错误码通常使用整数类型表示,缺乏类型安全性。
Expected<T, E>
类型明确表示函数可能返回类型为 T
的值,或者类型为 E
的错误。错误类型 E
可以是任意类型,例如枚举、类等,提供了类型安全保障。
② 显式性与可读性:
使用错误码需要手动检查返回值或输出参数来判断是否发生了错误,开发者可能会忘记检查错误码,导致错误被忽略。错误码的含义通常需要查阅文档才能理解,可读性较差。
Expected
的函数签名明确声明了可能返回错误,强制开发者显式地处理错误。Expected
类型和函数式操作符提高了代码的可读性,使得错误处理逻辑更加清晰易懂。
③ 函数式编程与错误信息:
错误码机制难以进行函数式编程,因为错误码通常是整数类型,无法直接进行函数式操作。错误码通常只包含错误代码,缺乏详细的错误信息,不利于错误诊断和调试。
Expected
提供了丰富的函数式操作符,可以方便地进行函数式编程。错误类型 E
可以是任意类型,可以包含详细的错误信息,例如错误消息、错误上下文等,有助于错误诊断和调试。
④ 代码维护性:
使用错误码时,错误码检查代码分散在各个函数调用处,代码维护性较差。
Expected
通过函数式操作符和链式调用,将错误处理逻辑集中起来,提高了代码的维护性。
总结:
与错误码相比,Expected
在类型安全、可读性、函数式编程和错误信息方面具有明显的优势。Expected
能够提供更清晰、更安全、更易维护的错误处理方案。在现代 C++ 开发中,尤其是在需要函数式编程和高性能的场景下,Expected
是比错误码更优秀的选择。
1.3.3 与 std::optional
的对比
std::optional
是 C++17 引入的一个用于表示可选值的类型。它可以用来处理函数可能返回空值的情况,但它与 Expected
在设计目标和适用场景上有所不同。
特性 | std::optional<T> | Expected<T, E> |
---|---|---|
设计目标 | 表示值可能缺失 (有值或无值) | 表示操作可能失败 (值或错误) |
错误表示 | 无值状态 (通过 std::nullopt 或 !optional 表示) | 错误类型 E 表示错误信息 |
错误信息 | 无法传递错误信息 | 可以传递任意类型的错误信息 (通过错误类型 E ) |
适用场景 | 函数可能返回空值、可选参数、缓存等 | 函数可能失败、需要明确区分正常结果和错误、错误处理 |
函数式操作 | 提供有限的函数式操作 (如 map , and_then ) | 提供丰富的函数式操作 (如 map , mapError , andThen , orElse , transform ) |
错误处理能力 | 错误处理能力有限 | 错误处理能力强大 |
① 设计目标与错误表示:
std::optional
的设计目标是表示值可能缺失,即一个变量可能包含一个类型为 T
的值,或者不包含任何值(空值)。std::optional
通过 std::nullopt
或 !optional
来表示无值状态。
Expected
的设计目标是表示操作可能失败,即一个函数可能返回一个类型为 T
的值(表示成功),或者返回一个类型为 E
的错误(表示失败)。Expected
通过错误类型 E
来表示错误信息。
② 错误信息与适用场景:
std::optional
只能表示“有值”或“无值”两种状态,无法传递关于错误类型和错误信息的详细描述。
Expected
可以通过错误类型 E
传递任意类型的错误信息,例如错误代码、错误消息、错误上下文等。
std::optional
适用于函数可能返回空值、可选参数、缓存等场景,主要关注值的缺失。
Expected
适用于函数可能失败、需要明确区分正常结果和错误、需要进行错误处理的场景,主要关注错误的处理和传递。
③ 函数式操作与错误处理能力:
std::optional
提供了一些基本的函数式操作,如 map
和 and_then
,但其函数式操作符相对有限。错误处理能力也比较有限,主要用于处理值的缺失情况。
Expected
提供了丰富的函数式操作符,如 map
、mapError
、andThen
、orElse
、transform
等,具有强大的错误处理能力。可以方便地进行链式调用、错误转换、错误恢复等操作。
总结:
std::optional
和 Expected
虽然都用于处理函数可能不返回有效值的情况,但它们的设计目标和适用场景有所不同。std::optional
主要用于表示值的缺失,而 Expected
主要用于表示操作的失败和错误的处理。在需要明确区分正常结果和错误,并需要传递错误信息的错误处理场景下,Expected
是比 std::optional
更合适的选择。可以将 std::optional
看作是 Expected<T, std::monostate>
的特例,其中 std::monostate
表示没有错误信息。
1.4 快速上手:你的第一个 Expected (Quick Start: Your First Expected)
本节将通过一个简单的示例,引导你快速上手 folly::Expected
的基本用法,包括如何包含头文件、创建 Expected
对象、检查 Expected
的状态以及访问 Expected
的值或错误。
1.4.1 包含头文件 (Include Header File)
要使用 folly::Expected
,首先需要在你的 C++ 代码中包含头文件 <folly/Expected.h>
。
1
#include <folly/Expected.h>
通常还需要包含 <system_error>
头文件,以便使用 std::error_code
作为默认的错误类型。
1
#include <system_error>
完整的头文件包含如下:
1
#include <folly/Expected.h>
2
#include <system_error>
3
#include <iostream> // for std::cout, std::cerr
4
#include <string> // for std::string
5
6
using namespace folly; // 引入 folly 命名空间,方便使用 Expected
1.4.2 创建 Expected 对象 (Creating Expected Objects)
创建 Expected
对象有多种方式,可以根据不同的场景选择合适的方式。
① 创建包含值的 Expected
对象:
可以使用 Expected<T, E>
的构造函数,直接传入一个类型为 T
的值来创建一个包含值的 Expected
对象。
1
Expected<int, std::error_code> expectedValue(42); // 创建包含整数值 42 的 Expected 对象
2
Expected<std::string, std::error_code> expectedString("Hello"); // 创建包含字符串 "Hello" 的 Expected 对象
也可以使用 makeExpected()
函数模板来创建包含值的 Expected
对象,这种方式更加简洁。
1
auto expectedValue = makeExpected<int, std::error_code>(42); // 使用 makeExpected 创建包含整数值 42 的 Expected 对象
2
auto expectedString = makeExpected<std::string, std::error_code>("Hello"); // 使用 makeExpected 创建包含字符串 "Hello" 的 Expected 对象
② 创建包含错误的 Expected
对象:
可以使用 makeUnexpected()
函数模板来创建一个包含错误的 Expected
对象。需要传入一个类型为 E
的错误对象作为参数。
1
auto expectedError = makeUnexpected<int, std::error_code>(std::error_code(5, std::generic_category())); // 创建包含 std::error_code 错误的 Expected 对象
在这个例子中,我们创建了一个包含 std::error_code
错误的 Expected
对象,错误代码为 5,错误类别为 std::generic_category()
。
③ 从可能抛出异常的函数创建 Expected
对象:
可以使用 try_catch
块结合 Expected
来处理可能抛出异常的函数。
1
Expected<int, std::exception_ptr> divide(int a, int b) {
2
if (b == 0) {
3
return makeUnexpected(std::make_exception_ptr(std::runtime_error("Division by zero")));
4
}
5
return a / b;
6
}
在这个例子中,divide
函数在除数为零时,返回一个包含 std::exception_ptr
错误的 Expected
对象,否则返回包含计算结果的 Expected
对象。
1.4.3 检查 Expected 的状态 (Checking Expected Status)
创建 Expected
对象后,需要检查其状态,判断是包含值还是包含错误。Expected
提供了 hasValue()
和 hasError()
成员函数来检查状态。
① hasValue()
:
hasValue()
函数返回 true
如果 Expected
对象包含值,否则返回 false
(即包含错误)。
1
auto result = readFile("example.txt");
2
if (result.hasValue()) {
3
// Expected 包含值,操作成功
4
std::string content = result.value();
5
std::cout << "File content:\n" << content << std::endl;
6
} else {
7
// Expected 包含错误,操作失败
8
std::cerr << "Error reading file." << std::endl;
9
}
② hasError()
:
hasError()
函数返回 true
如果 Expected
对象包含错误,否则返回 false
(即包含值)。
1
auto result = readFile("non_existent_file.txt");
2
if (result.hasError()) {
3
// Expected 包含错误,操作失败
4
std::error_code error = result.error();
5
std::cerr << "Error reading file: " << error.message() << std::endl;
6
} else {
7
// Expected 包含值,操作成功
8
std::cout << "File read successfully." << std::endl;
9
}
1.4.4 访问 Expected 的值或错误 (Accessing Expected Value or Error)
根据 Expected
的状态,可以访问其包含的值或错误。Expected
提供了 value()
和 error()
成员函数来访问值和错误。
① value()
:
value()
函数用于访问 Expected
对象包含的值。在调用 value()
之前,必须先使用 hasValue()
检查 Expected
对象是否包含值。如果 Expected
对象包含错误,调用 value()
将会抛出异常。
1
auto result = readFile("example.txt");
2
if (result.hasValue()) {
3
std::string content = result.value(); // 安全访问值
4
std::cout << "File content:\n" << content << std::endl;
5
}
② error()
:
error()
函数用于访问 Expected
对象包含的错误。在调用 error()
之前,必须先使用 hasError()
检查 Expected
对象是否包含错误。如果 Expected
对象包含值,调用 error()
将会抛出异常。
1
auto result = readFile("non_existent_file.txt");
2
if (result.hasError()) {
3
std::error_code error = result.error(); // 安全访问错误
4
std::cerr << "Error reading file: " << error.message() << std::endl;
5
}
③ valueOr()
:
valueOr()
函数提供了一种安全访问值的方式,即使 Expected
对象包含错误,也不会抛出异常。valueOr()
接受一个默认值作为参数,如果 Expected
对象包含值,则返回该值,否则返回默认值。
1
auto result = readFile("non_existent_file.txt");
2
std::string content = result.valueOr("Default Content"); // 提供默认值
3
std::cout << "File content:\n" << content << std::endl; // 即使读取失败,也不会抛出异常,content 将为 "Default Content"
④ errorOr()
:
errorOr()
函数与 valueOr()
类似,但它用于访问错误。errorOr()
接受一个默认错误作为参数,如果 Expected
对象包含错误,则返回该错误,否则返回默认错误。
1
auto result = readFile("example.txt");
2
std::error_code error = result.errorOr(std::error_code()); // 提供默认错误
3
if (result.hasError()) {
4
std::cerr << "Error: " << error.message() << std::endl; // 如果读取成功,error 将为默认错误 (通常表示无错误)
5
} else {
6
std::cout << "File read successfully." << std::endl;
7
}
通过本节的快速上手指南,你已经了解了 folly::Expected
的基本用法,包括如何创建 Expected
对象、检查状态以及访问值或错误。在接下来的章节中,我们将深入探讨 Expected
的更多高级特性和应用场景。
END_OF_CHAPTER
2. chapter 2:Expected 的基本操作 (Basic Operations of Expected)
2.1 构造与赋值 (Construction and Assignment)
folly::Expected
提供了多种构造函数和赋值操作符,以便于灵活地创建和操作 Expected
对象。理解这些构造和赋值方式对于正确使用 Expected
至关重要。本节将详细介绍 Expected
的各种构造函数和赋值操作,并通过代码示例进行说明。
2.1.1 默认构造函数 (Default Constructor)
默认构造函数创建一个 未初始化 的 Expected
对象。这意味着默认构造的 Expected
对象既不包含值,也不包含错误。因此,默认构造函数通常不应该直接使用,除非你稍后会显式地为 Expected
对象赋值。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
int main() {
5
folly::Expected<int, std::string> expected_default;
6
7
if (expected_default.hasValue()) {
8
std::cout << "Expected has value: " << expected_default.value() << std::endl;
9
} else if (expected_default.hasError()) {
10
std::cout << "Expected has error: " << expected_default.error() << std::endl;
11
} else {
12
std::cout << "Expected is uninitialized." << std::endl;
13
}
14
return 0;
15
}
代码解释:
⚝ 我们声明了一个 folly::Expected<int, std::string>
类型的对象 expected_default
,并使用默认构造函数进行初始化。
⚝ 我们使用 hasValue()
和 hasError()
检查 expected_default
的状态。
⚝ 由于默认构造的 Expected
对象是未初始化的,hasValue()
和 hasError()
都会返回 false
。因此,程序会输出 "Expected is uninitialized."。
注意: 默认构造的 Expected
对象处于一种特殊的状态,尝试直接访问其值或错误会导致未定义行为。在实际应用中,应避免使用默认构造函数,除非有明确的理由,并且确保在使用前显式地赋予其值或错误。
2.1.2 值构造函数 (Value Constructor)
值构造函数允许你使用一个值来构造 Expected
对象。当使用值构造函数时,Expected
对象将处于 包含值 的状态。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
int main() {
5
folly::Expected<int, std::string> expected_value(42);
6
7
if (expected_value.hasValue()) {
8
std::cout << "Expected has value: " << expected_value.value() << std::endl;
9
} else {
10
std::cout << "Expected has error." << std::endl;
11
}
12
return 0;
13
}
代码解释:
⚝ 我们使用值 42
构造了一个 folly::Expected<int, std::string>
对象 expected_value
。
⚝ hasValue()
返回 true
,表明 expected_value
包含一个值。
⚝ value()
函数返回 Expected
对象中存储的值 42
。
适用场景: 当你有一个正常的结果值,并希望将其封装到 Expected
对象中时,可以使用值构造函数。这通常用于表示函数成功执行并返回有效结果的情况。
2.1.3 错误构造函数 (Error Constructor)
错误构造函数允许你使用一个错误值来构造 Expected
对象。当使用错误构造函数时,Expected
对象将处于 包含错误 的状态。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
5
int main() {
6
folly::Expected<int, std::string> expected_error(folly::makeUnexpected("Something went wrong!"));
7
8
if (expected_error.hasError()) {
9
std::cout << "Expected has error: " << expected_error.error() << std::endl;
10
} else {
11
std::cout << "Expected has value." << std::endl;
12
}
13
return 0;
14
}
代码解释:
⚝ 我们使用 folly::makeUnexpected("Something went wrong!")
创建了一个 unexpected value,并用它构造了 folly::Expected<int, std::string>
对象 expected_error
。
⚝ folly::makeUnexpected
是一个辅助函数,用于创建表示错误的值,其类型与 Expected
的第二个模板参数(错误类型)相匹配。
⚝ hasError()
返回 true
,表明 expected_error
包含一个错误。
⚝ error()
函数返回 Expected
对象中存储的错误信息 "Something went wrong!"。
适用场景: 当函数执行失败并产生错误信息时,可以使用错误构造函数将错误信息封装到 Expected
对象中。这使得函数可以清晰地返回错误状态,而不是抛出异常或返回特殊的错误码。
注意: folly::makeUnexpected
是创建错误 Expected
对象的推荐方式,因为它类型安全且易于使用。
2.1.4 移动构造与移动赋值 (Move Construction and Move Assignment)
Expected
支持移动构造函数和移动赋值操作符,这对于处理大型对象或需要高效资源转移的场景非常重要。移动操作避免了不必要的拷贝,提高了性能。
移动构造函数
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
5
folly::Expected<std::string, std::string> create_expected_value() {
6
std::string value = "Large String Value";
7
return folly::Expected<std::string, std::string>(std::move(value)); // 移动构造
8
}
9
10
int main() {
11
folly::Expected<std::string, std::string> expected1 = create_expected_value(); // 移动构造
12
folly::Expected<std::string, std::string> expected2 = std::move(expected1); // 显式移动构造
13
14
std::cout << "expected2 has value: " << expected2.value() << std::endl;
15
return 0;
16
}
代码解释:
⚝ create_expected_value
函数返回一个 Expected<std::string, std::string>
对象,其中包含一个大型字符串。
⚝ 在 create_expected_value
函数内部,我们使用 std::move(value)
将局部变量 value
移动到 Expected
对象中。
⚝ 在 main
函数中,folly::Expected<std::string, std::string> expected1 = create_expected_value();
这里发生了隐式移动构造,因为 create_expected_value()
返回的是右值。
⚝ folly::Expected<std::string, std::string> expected2 = std::move(expected1);
这里使用了 std::move
显式地触发移动构造,将 expected1
的资源移动到 expected2
。
移动赋值操作符
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
5
int main() {
6
folly::Expected<std::string, std::string> expected1("Initial Value");
7
folly::Expected<std::string, std::string> expected2("Another Value");
8
9
expected2 = std::move(expected1); // 移动赋值
10
11
std::cout << "expected2 has value: " << expected2.value() << std::endl;
12
return 0;
13
}
代码解释:
⚝ 我们首先创建了两个 Expected
对象 expected1
和 expected2
,分别包含不同的字符串值。
⚝ expected2 = std::move(expected1);
使用 std::move
将 expected1
移动赋值给 expected2
。这会将 expected1
中包含的值(或错误)移动到 expected2
,而 expected1
将变为有效但未指定的状态。
性能优势: 移动构造和移动赋值避免了深拷贝,尤其是在 Expected
包含大型对象(例如 std::string
, std::vector
等)时,性能提升非常显著。它们通过转移资源所有权,而不是复制资源,来实现高效的对象转移。
2.1.5 拷贝构造与拷贝赋值 (Copy Construction and Copy Assignment)
Expected
也支持拷贝构造函数和拷贝赋值操作符,用于创建 Expected
对象的副本。拷贝操作会复制 Expected
对象中包含的值或错误。
拷贝构造函数
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
5
int main() {
6
folly::Expected<std::string, std::string> expected1("Original Value");
7
folly::Expected<std::string, std::string> expected2 = expected1; // 拷贝构造
8
9
std::cout << "expected2 has value: " << expected2.value() << std::endl;
10
return 0;
11
}
代码解释:
⚝ folly::Expected<std::string, std::string> expected2 = expected1;
使用 expected1
拷贝构造 expected2
。这将创建一个新的 Expected
对象 expected2
,其内容是 expected1
的副本。
拷贝赋值操作符
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
5
int main() {
6
folly::Expected<std::string, std::string> expected1("Original Value");
7
folly::Expected<std::string, std::string> expected2("Another Value");
8
9
expected2 = expected1; // 拷贝赋值
10
11
std::cout << "expected2 has value: " << expected2.value() << std::endl;
12
return 0;
13
}
代码解释:
⚝ expected2 = expected1;
使用 expected1
拷贝赋值给 expected2
。这将把 expected1
中包含的值(或错误)复制到 expected2
,覆盖 expected2
原有的内容。
深拷贝与浅拷贝: 对于 Expected
来说,拷贝构造和拷贝赋值执行的是深拷贝。这意味着如果 Expected
包含的是对象,那么会复制对象本身,而不仅仅是对象的指针或引用。这确保了拷贝后的 Expected
对象与原始对象相互独立。
选择移动还是拷贝: 在性能敏感的场景中,应优先考虑使用移动操作,特别是当 Expected
包含大型对象时。拷贝操作会带来额外的性能开销,因为它需要复制对象。只有在需要保留原始 Expected
对象的情况下,才应该使用拷贝操作。
2.2 状态检查 (Status Checking)
在使用 Expected
对象时,首先需要检查其状态,即它是否包含一个值或一个错误。Expected
提供了 hasValue()
和 hasError()
成员函数来执行状态检查。
2.2.1 hasValue()
:检查是否包含值 (Check if Contains Value)
hasValue()
函数返回一个 bool
值,指示 Expected
对象是否包含一个值。如果 Expected
对象处于 包含值 的状态,hasValue()
返回 true
;否则返回 false
。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
folly::Expected<int, std::string> get_result(bool success) {
5
if (success) {
6
return folly::Expected<int, std::string>(100);
7
} else {
8
return folly::makeUnexpected("Operation failed");
9
}
10
}
11
12
int main() {
13
folly::Expected<int, std::string> result1 = get_result(true);
14
folly::Expected<int, std::string> result2 = get_result(false);
15
16
if (result1.hasValue()) {
17
std::cout << "result1 has value." << std::endl; // 输出
18
} else {
19
std::cout << "result1 has error." << std::endl;
20
}
21
22
if (result2.hasValue()) {
23
std::cout << "result2 has value." << std::endl;
24
} else {
25
std::cout << "result2 has error." << std::endl; // 输出
26
}
27
28
return 0;
29
}
代码解释:
⚝ get_result
函数根据 success
参数返回一个 Expected
对象。如果 success
为 true
,则返回包含值 100
的 Expected
;否则返回包含错误信息 "Operation failed" 的 Expected
。
⚝ 在 main
函数中,我们分别调用 get_result(true)
和 get_result(false)
获取 result1
和 result2
。
⚝ 对于 result1
,hasValue()
返回 true
,因为 result1
包含一个值。
⚝ 对于 result2
,hasValue()
返回 false
,因为 result2
包含一个错误。
2.2.2 hasError()
:检查是否包含错误 (Check if Contains Error)
hasError()
函数返回一个 bool
值,指示 Expected
对象是否包含一个错误。如果 Expected
对象处于 包含错误 的状态,hasError()
返回 true
;否则返回 false
。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
folly::Expected<int, std::string> get_result(bool success) {
5
if (success) {
6
return folly::Expected<int, std::string>(100);
7
} else {
8
return folly::makeUnexpected("Operation failed");
9
}
10
}
11
12
int main() {
13
folly::Expected<int, std::string> result1 = get_result(true);
14
folly::Expected<int, std::string> result2 = get_result(false);
15
16
if (result1.hasError()) {
17
std::cout << "result1 has error." << std::endl;
18
} else {
19
std::cout << "result1 has value." << std::endl; // 输出
20
}
21
22
if (result2.hasError()) {
23
std::cout << "result2 has error." << std::endl; // 输出
24
} else {
25
std::cout << "result2 has value." << std::endl;
26
}
27
28
return 0;
29
}
代码解释:
⚝ 与 hasValue()
的示例代码相同,我们使用 get_result
函数生成 result1
和 result2
。
⚝ 对于 result1
,hasError()
返回 false
,因为 result1
包含一个值。
⚝ 对于 result2
,hasError()
返回 true
,因为 result2
包含一个错误。
状态检查的重要性: 在访问 Expected
对象的值或错误之前,务必先进行状态检查。直接访问未初始化或处于错误状态的 Expected
对象的值或错误会导致未定义行为,通常是程序崩溃。hasValue()
和 hasError()
函数提供了安全的方式来确定 Expected
对象的状态,从而避免潜在的运行时错误。
2.3 值与错误的访问 (Accessing Value and Error)
在确认 Expected
对象的状态后,可以使用相应的成员函数来访问其包含的值或错误。Expected
提供了 value()
, error()
, valueOr()
, 和 errorOr()
等函数来安全地访问值和错误。
2.3.1 value()
:访问值 (Access Value)
value()
函数用于访问 Expected
对象中包含的值。调用 value()
之前,必须确保 Expected
对象处于包含值的状态,即 hasValue()
返回 true
。如果在 hasValue()
返回 false
的情况下调用 value()
,程序会抛出异常 folly::ExpectedBadAccessException
。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <stdexcept>
4
5
folly::Expected<int, std::string> get_result(bool success) {
6
if (success) {
7
return folly::Expected<int, std::string>(100);
8
} else {
9
return folly::makeUnexpected("Operation failed");
10
}
11
}
12
13
int main() {
14
folly::Expected<int, std::string> result1 = get_result(true);
15
folly::Expected<int, std::string> result2 = get_result(false);
16
17
if (result1.hasValue()) {
18
int val = result1.value();
19
std::cout << "Value: " << val << std::endl; // 输出 Value: 100
20
}
21
22
if (result2.hasValue()) {
23
// 错误用法!result2 包含错误,不应该调用 value()
24
// int val = result2.value(); // 会抛出 folly::ExpectedBadAccessException 异常
25
// std::cout << "Value: " << val << std::endl;
26
} else {
27
std::cout << "result2 has error, cannot access value." << std::endl; // 输出
28
}
29
30
return 0;
31
}
代码解释:
⚝ 对于 result1
,hasValue()
返回 true
,因此可以安全地调用 value()
获取值 100
。
⚝ 对于 result2
,hasValue()
返回 false
,如果取消注释 int val = result2.value();
这行代码,程序会抛出 folly::ExpectedBadAccessException
异常。
异常安全性: value()
函数的设计旨在提供快速访问值的途径,但不提供安全性。它假定调用者已经通过 hasValue()
检查确保了 Expected
对象包含值。如果需要更安全的值访问方式,请考虑使用 valueOr()
函数。
2.3.2 error()
:访问错误 (Access Error)
error()
函数用于访问 Expected
对象中包含的错误。调用 error()
之前,必须确保 Expected
对象处于包含错误的状态,即 hasError()
返回 true
。如果在 hasError()
返回 false
的情况下调用 error()
,程序会抛出异常 folly::ExpectedBadAccessException
。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <stdexcept>
4
5
folly::Expected<int, std::string> get_result(bool success) {
6
if (success) {
7
return folly::Expected<int, std::string>(100);
8
} else {
9
return folly::makeUnexpected("Operation failed");
10
}
11
}
12
13
int main() {
14
folly::Expected<int, std::string> result1 = get_result(true);
15
folly::Expected<int, std::string> result2 = get_result(false);
16
17
if (result2.hasError()) {
18
std::string err = result2.error();
19
std::cout << "Error: " << err << std::endl; // 输出 Error: Operation failed
20
}
21
22
if (result1.hasError()) {
23
// 错误用法!result1 包含值,不应该调用 error()
24
// std::string err = result1.error(); // 会抛出 folly::ExpectedBadAccessException 异常
25
// std::cout << "Error: " << err << std::endl;
26
} else {
27
std::cout << "result1 has value, cannot access error." << std::endl; // 输出
28
}
29
30
return 0;
31
}
代码解释:
⚝ 对于 result2
,hasError()
返回 true
,因此可以安全地调用 error()
获取错误信息 "Operation failed"。
⚝ 对于 result1
,hasError()
返回 false
,如果取消注释 std::string err = result1.error();
这行代码,程序会抛出 folly::ExpectedBadAccessException
异常。
异常安全性: 与 value()
类似,error()
函数也旨在提供快速访问错误的途径,但不提供安全性。它假定调用者已经通过 hasError()
检查确保了 Expected
对象包含错误。如果需要更安全的错误访问方式,请考虑使用 errorOr()
函数。
2.3.3 valueOr()
:提供默认值 (Provide Default Value)
valueOr()
函数提供了一种安全访问值的方式,即使 Expected
对象不包含值(即包含错误)。它接受一个默认值作为参数。如果 Expected
对象包含值,valueOr()
返回该值;如果 Expected
对象包含错误,valueOr()
返回提供的默认值。valueOr()
永远不会抛出异常。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
folly::Expected<int, std::string> get_result(bool success) {
5
if (success) {
6
return folly::Expected<int, std::string>(100);
7
} else {
8
return folly::makeUnexpected("Operation failed");
9
}
10
}
11
12
int main() {
13
folly::Expected<int, std::string> result1 = get_result(true);
14
folly::Expected<int, std::string> result2 = get_result(false);
15
16
int val1 = result1.valueOr(-1); // result1 包含值,返回 100
17
int val2 = result2.valueOr(-1); // result2 包含错误,返回默认值 -1
18
19
std::cout << "Value 1: " << val1 << std::endl; // 输出 Value 1: 100
20
std::cout << "Value 2: " << val2 << std::endl; // 输出 Value 2: -1
21
22
return 0;
23
}
代码解释:
⚝ 对于 result1
,valueOr(-1)
返回 result1
中包含的值 100
,因为 result1
包含值。
⚝ 对于 result2
,valueOr(-1)
返回默认值 -1
,因为 result2
包含错误。
安全性与便捷性: valueOr()
函数提供了一种安全且便捷的方式来访问 Expected
对象的值,尤其是在错误情况下需要提供一个合理的默认值时。它避免了显式的状态检查和异常处理,使代码更加简洁和健壮。
2.3.4 errorOr()
:提供默认错误 (Provide Default Error)
errorOr()
函数提供了一种安全访问错误的方式,即使 Expected
对象不包含错误(即包含值)。它接受一个默认错误作为参数。如果 Expected
对象包含错误,errorOr()
返回该错误;如果 Expected
对象包含值,errorOr()
返回提供的默认错误。errorOr()
永远不会抛出异常。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
5
folly::Expected<int, std::string> get_result(bool success) {
6
if (success) {
7
return folly::Expected<int, std::string>(100);
8
} else {
9
return folly::makeUnexpected("Operation failed");
10
}
11
}
12
13
int main() {
14
folly::Expected<int, std::string> result1 = get_result(true);
15
folly::Expected<int, std::string> result2 = get_result(false);
16
17
std::string err1 = result1.errorOr("No error"); // result1 包含值,返回默认错误 "No error"
18
std::string err2 = result2.errorOr("No error"); // result2 包含错误,返回错误信息 "Operation failed"
19
20
std::cout << "Error 1: " << err1 << std::endl; // 输出 Error 1: No error
21
std::cout << "Error 2: " << err2 << std::endl; // 输出 Error 2: Operation failed
22
23
return 0;
24
}
代码解释:
⚝ 对于 result1
,errorOr("No error")
返回默认错误信息 "No error",因为 result1
包含值。
⚝ 对于 result2
,errorOr("No error")
返回 result2
中包含的错误信息 "Operation failed",因为 result2
包含错误。
安全性与错误处理: errorOr()
函数在需要统一处理错误情况时非常有用。即使在正常情况下(Expected
包含值),它也能提供一个默认的错误值,避免了代码中出现分支判断。这在某些错误日志记录或错误上报场景中非常方便。
2.4 Expected 的转换 (Conversion of Expected)
Expected
对象可以转换为其他类型,以便于与其他 C++ 特性或库进行互操作。本节介绍 Expected
转换为 bool
类型以及与 std::optional
的互操作。
2.4.1 转换为 bool
类型 (Conversion to bool
Type)
Expected
对象可以隐式转换为 bool
类型。当 Expected
对象包含值时,转换为 true
;当 Expected
对象包含错误时,转换为 false
。这种转换使得 Expected
对象可以直接在条件语句中使用,简化了状态检查的代码。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
folly::Expected<int, std::string> get_result(bool success) {
5
if (success) {
6
return folly::Expected<int, std::string>(100);
7
} else {
8
return folly::makeUnexpected("Operation failed");
9
}
10
}
11
12
int main() {
13
folly::Expected<int, std::string> result1 = get_result(true);
14
folly::Expected<int, std::string> result2 = get_result(false);
15
16
if (result1) { // 隐式转换为 true,因为 result1 包含值
17
std::cout << "result1 is true (has value)." << std::endl; // 输出
18
} else {
19
std::cout << "result1 is false (has error)." << std::endl;
20
}
21
22
if (result2) {
23
std::cout << "result2 is true (has value)." << std::endl;
24
} else { // 隐式转换为 false,因为 result2 包含错误
25
std::cout << "result2 is false (has error)." << std::endl; // 输出
26
}
27
28
return 0;
29
}
代码解释:
⚝ if (result1)
和 if (result2)
语句直接将 Expected
对象作为条件判断。
⚝ 由于 result1
包含值,result1
隐式转换为 true
。
⚝ 由于 result2
包含错误,result2
隐式转换为 false
。
代码简洁性: 隐式转换为 bool
类型使得状态检查的代码更加简洁和直观,可以直接在 if
语句或循环条件中使用 Expected
对象,而无需显式调用 hasValue()
或 hasError()
。
2.4.2 与 std::optional
的互操作 (Interoperation with std::optional
)
folly::Expected
可以与 C++17 标准库中的 std::optional
进行互操作。std::optional
用于表示一个值可能存在也可能不存在的情况,类似于可以成功返回一个值或者不返回任何值的函数。Expected
可以转换为 std::optional
,丢失错误信息,只保留成功或失败的状态。
Expected
转换为 std::optional
可以使用 folly::Expected::as_optional()
成员函数将 Expected
对象转换为 std::optional
对象。如果 Expected
对象包含值,as_optional()
返回包含该值的 std::optional
对象;如果 Expected
对象包含错误,as_optional()
返回一个空的 std::optional
对象。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <optional>
4
5
folly::Expected<int, std::string> get_result(bool success) {
6
if (success) {
7
return folly::Expected<int, std::string>(100);
8
} else {
9
return folly::makeUnexpected("Operation failed");
10
}
11
}
12
13
int main() {
14
folly::Expected<int, std::string> result1 = get_result(true);
15
folly::Expected<int, std::string> result2 = get_result(false);
16
17
std::optional<int> optional1 = result1.as_optional();
18
std::optional<int> optional2 = result2.as_optional();
19
20
if (optional1.has_value()) {
21
std::cout << "optional1 has value: " << optional1.value() << std::endl; // 输出 Value: 100
22
} else {
23
std::cout << "optional1 is empty." << std::endl;
24
}
25
26
if (optional2.has_value()) {
27
std::cout << "optional2 has value: " << optional2.value() << std::endl;
28
} else {
29
std::cout << "optional2 is empty." << std::endl; // 输出 optional2 is empty.
30
}
31
32
return 0;
33
}
代码解释:
⚝ std::optional<int> optional1 = result1.as_optional();
将 result1
转换为 std::optional<int>
。由于 result1
包含值,optional1
也包含该值。
⚝ std::optional<int> optional2 = result2.as_optional();
将 result2
转换为 std::optional<int>
。由于 result2
包含错误,optional2
变为空。
适用场景: 当只需要表示操作成功或失败,而不需要传递具体的错误信息时,可以将 Expected
转换为 std::optional
。这在与只接受 std::optional
的库或接口进行交互时非常有用。
注意: 从 Expected
转换为 std::optional
会丢失错误信息。如果需要保留错误信息,则不应进行此转换。
总结: 本章详细介绍了 folly::Expected
的基本操作,包括构造与赋值、状态检查、值与错误的访问以及类型转换。掌握这些基本操作是有效使用 Expected
的基础,也是理解后续高级功能和应用场景的前提。通过合理地使用这些基本操作,可以编写出更加健壮、可读性更强的 C++ 代码。
END_OF_CHAPTER
3. chapter 3:Expected 的函数式操作 (Functional Operations of Expected)
3.1 map():转换值 (Transform Value)
Expected
的 map()
方法是一种函数式操作,允许你在 Expected
对象包含值时,对该值应用一个函数进行转换,并返回一个新的 Expected
对象。如果原始 Expected
对象包含错误,map()
操作将不会应用转换函数,而是直接返回包含相同错误的新 Expected
对象。这使得 map()
成为处理可能成功或失败的操作链中的值转换的强大工具。
3.1.1 map() 的基本用法 (Basic Usage of map())
map()
方法接受一个函数作为参数。这个函数会被应用到 Expected
对象内部的值(如果存在)。函数的签名应该是接受一个与 Expected
存储值类型相同的参数,并返回一个新值。map()
的返回值是一个新的 Expected
对象,其值类型可能与原始 Expected
对象不同,取决于转换函数的返回类型。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
Expected<int, std::string> safe_divide(int a, int b) {
8
if (b == 0) {
9
return makeUnexpected("Division by zero");
10
}
11
return a / b;
12
}
13
14
int multiply_by_two(int x) {
15
return x * 2;
16
}
17
18
int main() {
19
auto result1 = safe_divide(10, 2).map(multiply_by_two);
20
if (result1.hasValue()) {
21
std::cout << "Result 1: " << result1.value() << std::endl; // 输出: Result 1: 10
22
} else {
23
std::cout << "Error 1: " << result1.error() << std::endl;
24
}
25
26
auto result2 = safe_divide(5, 0).map(multiply_by_two);
27
if (result2.hasValue()) {
28
std::cout << "Result 2: " << result2.value() << std::endl;
29
} else {
30
std::cout << "Error 2: " << result2.error() << std::endl; // 输出: Error 2: Division by zero
31
}
32
33
return 0;
34
}
在这个例子中,safe_divide
函数返回一个 Expected<int, std::string>
,表示除法操作的结果。map(multiply_by_two)
将 multiply_by_two
函数应用到 safe_divide
的结果值上(如果 safe_divide
成功返回一个值)。
⚝ 当 safe_divide(10, 2)
成功返回 Expected<5, std::string>
时,map(multiply_by_two)
将 multiply_by_two(5)
应用于值 5
,得到 10
,最终 result1
包含 Expected<10, std::string>
。
⚝ 当 safe_divide(5, 0)
返回一个包含错误的 Expected
时,map(multiply_by_two)
不会执行 multiply_by_two
函数,而是直接将错误传递下去,result2
仍然包含原始的错误信息 "Division by zero"。
3.1.2 map() 的链式调用 (Chaining map())
map()
操作可以链式调用,这使得可以对 Expected
对象的值进行一系列的转换,而无需显式地检查每一步操作是否成功。这种链式调用是函数式编程风格的核心,可以提高代码的简洁性和可读性。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
Expected<int, std::string> safe_divide(int a, int b) {
8
if (b == 0) {
9
return makeUnexpected("Division by zero");
10
}
11
return a / b;
12
}
13
14
int multiply_by_two(int x) {
15
return x * 2;
16
}
17
18
std::string int_to_string(int x) {
19
return std::to_string(x);
20
}
21
22
int main() {
23
auto result = safe_divide(20, 2)
24
.map(multiply_by_two)
25
.map(int_to_string);
26
27
if (result.hasValue()) {
28
std::cout << "Final Result: " << result.value() << std::endl; // 输出: Final Result: 20
29
} else {
30
std::cout << "Error: " << result.error() << std::endl;
31
}
32
33
auto error_result = safe_divide(10, 0)
34
.map(multiply_by_two)
35
.map(int_to_string);
36
37
if (error_result.hasValue()) {
38
std::cout << "Final Result: " << error_result.value() << std::endl;
39
} else {
40
std::cout << "Error: " << error_result.error() << std::endl; // 输出: Error: Division by zero
41
}
42
43
return 0;
44
}
在这个例子中,我们首先使用 safe_divide(20, 2)
得到一个 Expected<int, std::string>
,然后链式调用了两次 map()
:
① .map(multiply_by_two)
将值乘以 2。
② .map(int_to_string)
将整数转换为字符串。
如果 safe_divide
操作成功,那么这两个 map()
操作会依次应用,最终 result
将包含一个 Expected<std::string, std::string>
,其值为字符串 "20"。如果 safe_divide
操作失败(例如 safe_divide(10, 0)
),那么 map()
链中的转换函数都不会被执行,错误信息会直接传递到 error_result
。
3.2 mapError():转换错误 (Transform Error)
与 map()
类似,mapError()
方法允许你转换 Expected
对象中包含的错误值。如果 Expected
对象包含的是值而不是错误,mapError()
操作将不会应用转换函数,而是直接返回包含原始值的新 Expected
对象。mapError()
对于统一错误类型、添加错误上下文信息或者将错误转换为更易于处理的形式非常有用。
3.2.1 mapError() 的基本用法 (Basic Usage of mapError())
mapError()
方法接受一个函数作为参数,这个函数会被应用到 Expected
对象内部的错误值(如果存在)。函数的签名应该是接受一个与 Expected
存储错误类型相同的参数,并返回一个新的错误值。mapError()
的返回值是一个新的 Expected
对象,其错误类型可能与原始 Expected
对象不同,取决于错误转换函数的返回类型。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
enum class ErrorCode {
8
DIVISION_BY_ZERO,
9
INVALID_INPUT
10
};
11
12
Expected<int, ErrorCode> safe_divide_enum(int a, int b) {
13
if (b == 0) {
14
return makeUnexpected(ErrorCode::DIVISION_BY_ZERO);
15
}
16
return a / b;
17
}
18
19
std::string error_code_to_string(ErrorCode error) {
20
switch (error) {
21
case ErrorCode::DIVISION_BY_ZERO:
22
return "Error: Division by zero";
23
case ErrorCode::INVALID_INPUT:
24
return "Error: Invalid input";
25
default:
26
return "Unknown error";
27
}
28
}
29
30
int main() {
31
auto result1 = safe_divide_enum(10, 2).mapError(error_code_to_string);
32
if (result1.hasValue()) {
33
std::cout << "Result 1: " << result1.value() << std::endl; // 输出: Result 1: 5
34
} else {
35
std::cout << "Error 1: " << result1.error() << std::endl;
36
}
37
38
auto result2 = safe_divide_enum(5, 0).mapError(error_code_to_string);
39
if (result2.hasValue()) {
40
std::cout << "Result 2: " << result2.value() << std::endl;
41
} else {
42
std::cout << "Error 2: " << result2.error() << std::endl; // 输出: Error 2: Error: Division by zero
43
}
44
45
return 0;
46
}
在这个例子中,safe_divide_enum
函数返回一个 Expected<int, ErrorCode>
,使用枚举类型 ErrorCode
来表示错误。mapError(error_code_to_string)
将 error_code_to_string
函数应用到 safe_divide_enum
返回的错误值上(如果 safe_divide_enum
返回一个错误)。
⚝ 当 safe_divide_enum(10, 2)
成功返回 Expected<5, ErrorCode>
时,mapError(error_code_to_string)
不会执行任何操作,result1
仍然包含值 5
。
⚝ 当 safe_divide_enum(5, 0)
返回一个包含错误 ErrorCode::DIVISION_BY_ZERO
的 Expected
时,mapError(error_code_to_string)
将 error_code_to_string(ErrorCode::DIVISION_BY_ZERO)
应用于错误值,将其转换为字符串 "Error: Division by zero",最终 result2
包含一个 Expected<int, std::string>
,其错误信息为字符串。
3.2.2 mapError() 的应用场景 (Application Scenarios of mapError())
mapError()
在多种场景下都非常有用,尤其是在需要统一错误处理或者提供更详细错误信息时。
① 错误类型转换 (Error Type Conversion):当你的代码库的不同部分使用不同的错误类型时,mapError()
可以用来将错误转换为统一的类型,方便上层处理。例如,将底层的特定错误码转换为更通用的错误枚举或错误信息字符串。
② 添加错误上下文 (Adding Error Context):在错误传递的过程中,你可能需要在错误信息中添加更多的上下文信息,例如操作的名称、参数值等。mapError()
可以在不丢失原始错误信息的情况下,添加这些上下文信息。
③ 本地化错误信息 (Localizing Error Messages):如果你的应用需要支持多语言,mapError()
可以用来根据用户的语言设置,将错误信息转换为相应的本地化版本。
④ 错误重试或降级处理 (Error Retry or Fallback):在某些情况下,你可能希望根据具体的错误类型来决定是否重试操作,或者采取降级处理措施。mapError()
可以将错误类型转换为更易于判断和处理的形式。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
enum class FileError {
8
FILE_NOT_FOUND,
9
PERMISSION_DENIED,
10
DISK_FULL
11
};
12
13
Expected<std::string, FileError> read_file_content(const std::string& filename) {
14
// 模拟文件读取失败
15
if (filename == "config.txt") {
16
return makeUnexpected(FileError::FILE_NOT_FOUND);
17
}
18
return "File content";
19
}
20
21
std::string enhance_file_error_message(FileError error) {
22
switch (error) {
23
case FileError::FILE_NOT_FOUND:
24
return "Failed to read file: File not found.";
25
case FileError::PERMISSION_DENIED:
26
return "Failed to read file: Permission denied.";
27
case FileError::DISK_FULL:
28
return "Failed to read file: Disk full.";
29
default:
30
return "Failed to read file: Unknown error.";
31
}
32
}
33
34
int main() {
35
auto result1 = read_file_content("data.txt").mapError(enhance_file_error_message);
36
if (result1.hasValue()) {
37
std::cout << "File content: " << result1.value() << std::endl; // 输出: File content: File content
38
} else {
39
std::cout << "Error 1: " << result1.error() << std::endl;
40
}
41
42
auto result2 = read_file_content("config.txt").mapError(enhance_file_error_message);
43
if (result2.hasValue()) {
44
std::cout << "File content: " << result2.value() << std::endl;
45
} else {
46
std::cout << "Error 2: " << result2.error() << std::endl; // 输出: Error 2: Failed to read file: File not found.
47
}
48
49
return 0;
50
}
在这个例子中,read_file_content
函数模拟读取文件内容,并可能返回 FileError
类型的错误。mapError(enhance_file_error_message)
使用 enhance_file_error_message
函数将 FileError
枚举值转换为更友好的、包含上下文信息的错误字符串。这样,当文件读取失败时,用户可以得到更清晰的错误提示。
3.3 andThen() (flatMap):链式操作与结果组合 (Chaining Operations and Combining Results)
andThen()
,有时也称为 flatMap
,是 Expected
中最强大的函数式操作之一。它允许你链式调用可能返回 Expected
的函数,并将它们的结果组合起来。与 map()
不同,andThen()
接受的转换函数本身就返回一个 Expected
对象。这使得 andThen()
非常适合处理一系列依赖于前一步操作结果的、可能失败的操作。
3.3.1 andThen() 的基本用法 (Basic Usage of andThen())
andThen()
方法接受一个函数作为参数,这个函数会被应用到 Expected
对象内部的值(如果存在)。关键在于,这个函数必须返回一个 Expected
对象。andThen()
会“展平”嵌套的 Expected
,确保最终的结果仍然是一个单层的 Expected
。如果原始 Expected
对象包含错误,andThen()
操作将不会应用转换函数,而是直接返回包含相同错误的新 Expected
对象。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
Expected<int, std::string> safe_divide(int a, int b) {
8
if (b == 0) {
9
return makeUnexpected("Division by zero");
10
}
11
return a / b;
12
}
13
14
Expected<int, std::string> safe_multiply(int a, int b) {
15
if (a > 100 || b > 100) {
16
return makeUnexpected("Multiplication operands too large");
17
}
18
return a * b;
19
}
20
21
int main() {
22
auto result1 = safe_divide(20, 2).andThen([&](int divided_value) {
23
return safe_multiply(divided_value, 5);
24
});
25
26
if (result1.hasValue()) {
27
std::cout << "Result 1: " << result1.value() << std::endl; // 输出: Result 1: 50
28
} else {
29
std::cout << "Error 1: " << result1.error() << std::endl;
30
}
31
32
auto result2 = safe_divide(10, 0).andThen([&](int divided_value) {
33
return safe_multiply(divided_value, 5);
34
});
35
36
if (result2.hasValue()) {
37
std::cout << "Result 2: " << result2.value() << std::endl;
38
} else {
39
std::cout << "Error 2: " << result2.error() << std::endl; // 输出: Error 2: Division by zero
40
}
41
42
auto result3 = safe_divide(200, 2).andThen([&](int divided_value) {
43
return safe_multiply(divided_value, 5);
44
});
45
46
if (result3.hasValue()) {
47
std::cout << "Result 3: " << result3.value() << std::endl;
48
} else {
49
std::cout << "Error 3: " << result3.error() << std::endl; // 输出: Error 3: Multiplication operands too large
50
}
51
52
return 0;
53
}
在这个例子中,我们定义了两个可能失败的函数 safe_divide
和 safe_multiply
,它们都返回 Expected<int, std::string>
。我们使用 andThen()
将这两个操作链接起来:
① safe_divide(20, 2)
的结果(如果成功)会被传递给 lambda 函数。
② lambda 函数调用 safe_multiply(divided_value, 5)
,并返回其结果。
如果 safe_divide
失败,andThen()
会直接返回 safe_divide
的错误。如果 safe_divide
成功,但 safe_multiply
失败,andThen()
会返回 safe_multiply
的错误。只有当两个操作都成功时,result1
才会包含最终的计算结果。
3.3.2 andThen() 的 Monadic 特性 (Monadic Properties of andThen())
andThen()
操作体现了 Monad 的特性,Monad 是函数式编程中的一个重要概念,用于处理链式计算和副作用。在 Expected
的上下文中,andThen()
使得 Expected
成为一个 Monad。Monad 的核心思想是“链式绑定”操作,它允许你将多个可能失败的操作串联起来,形成一个计算管道。
andThen()
的 Monadic 特性主要体现在以下几个方面:
① 顺序执行 (Sequential Execution):andThen()
确保操作按照链式顺序执行。只有前一个操作成功,才会执行下一个操作。如果任何一个操作失败,整个链式计算会立即终止,并返回第一个失败操作的错误。
② 错误传播 (Error Propagation):错误信息会在 andThen()
链中自动传播。如果链中的任何一个环节产生错误,这个错误会被传递到最终的 Expected
结果中,而不会被“吞噬”或忽略。
③ 类型安全 (Type Safety):andThen()
是类型安全的。它要求转换函数返回一个 Expected
对象,并且会根据返回的 Expected
对象的类型来确定链式调用的结果类型。这避免了类型不匹配和运行时错误。
④ 代码简洁性 (Code Conciseness):使用 andThen()
可以将复杂的、可能失败的操作链表示为简洁、易读的代码。避免了大量的嵌套条件判断和错误检查代码,提高了代码的可维护性。
Monadic 特性使得 andThen()
不仅仅是一个简单的链式调用工具,更是一种强大的控制流机制,用于处理复杂的、可能出错的业务逻辑。通过 andThen()
,你可以以声明式的方式组织代码,将关注点从错误处理的细节中解放出来,更专注于业务逻辑的实现。
3.4 orElse():处理错误情况 (Handling Error Cases)
orElse()
方法提供了一种处理 Expected
对象包含错误情况的方式。当 Expected
对象包含错误时,orElse()
允许你提供一个备选的 Expected
对象作为结果。如果原始 Expected
对象包含值,orElse()
操作将直接返回原始 Expected
对象,而不会执行备选操作。orElse()
对于错误恢复、提供默认值或者执行清理操作非常有用。
3.4.1 orElse() 的基本用法 (Basic Usage of orElse())
orElse()
方法接受一个函数或者一个 Expected
对象作为参数。
⚝ 接受函数:如果 orElse()
接受一个函数,这个函数会在原始 Expected
对象包含错误时被调用。这个函数必须返回一个 Expected
对象,作为备选结果。
⚝ 接受 Expected
对象:如果 orElse()
接受一个 Expected
对象,当原始 Expected
对象包含错误时,直接返回这个备选 Expected
对象。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
Expected<int, std::string> safe_divide(int a, int b) {
8
if (b == 0) {
9
return makeUnexpected("Division by zero");
10
}
11
return a / b;
12
}
13
14
Expected<int, std::string> default_value() {
15
return 0; // 返回一个包含默认值的 Expected
16
}
17
18
int main() {
19
auto result1 = safe_divide(10, 2).orElse(default_value);
20
if (result1.hasValue()) {
21
std::cout << "Result 1: " << result1.value() << std::endl; // 输出: Result 1: 5
22
} else {
23
std::cout << "Error 1: " << result1.error() << std::endl;
24
}
25
26
auto result2 = safe_divide(5, 0).orElse(default_value);
27
if (result2.hasValue()) {
28
std::cout << "Result 2: " << result2.value() << std::endl; // 输出: Result 2: 0
29
} else {
30
std::cout << "Error 2: " << result2.error() << std::endl;
31
}
32
33
auto result3 = safe_divide(5, 0).orElse(makeUnexpected("Fallback failed"));
34
if (result3.hasValue()) {
35
std::cout << "Result 3: " << result3.value() << std::endl;
36
} else {
37
std::cout << "Error 3: " << result3.error() << std::endl; // 输出: Error 3: Fallback failed
38
}
39
40
return 0;
41
}
在这个例子中:
⚝ safe_divide(10, 2).orElse(default_value)
:由于 safe_divide
成功,orElse
没有执行,result1
包含 safe_divide
的结果值 5
。
⚝ safe_divide(5, 0).orElse(default_value)
:由于 safe_divide
失败,orElse(default_value)
被调用,返回 default_value()
的结果,即包含默认值 0
的 Expected
,因此 result2
包含值 0
。
⚝ safe_divide(5, 0).orElse(makeUnexpected("Fallback failed"))
:当 safe_divide
失败时,orElse
返回了备选的 Expected
对象 makeUnexpected("Fallback failed")
,导致 result3
包含新的错误信息 "Fallback failed"。
3.4.2 orElse() 的错误恢复 (Error Recovery with orElse())
orElse()
的主要应用场景之一是错误恢复。当某个操作可能失败时,你可以使用 orElse()
提供一个备选方案,使得程序在遇到错误时能够继续执行,而不是立即终止。错误恢复可以采取多种形式:
① 提供默认值 (Providing Default Values):如上例所示,当操作失败时,可以使用 orElse()
返回一个包含默认值的 Expected
对象。这在某些情况下可以保证程序的健壮性,例如,当从配置文件读取参数失败时,可以使用默认参数。
② 重试操作 (Retrying Operations):如果错误是暂时性的(例如网络连接错误),可以使用 orElse()
在错误发生时重试操作。重试逻辑可以在 orElse()
提供的函数中实现。
③ 降级处理 (Fallback to Alternative Logic):当主要逻辑失败时,可以使用 orElse()
切换到备选的、性能较低或者功能较弱的逻辑。例如,当使用高性能缓存失败时,可以降级到直接访问数据库。
④ 记录错误并继续 (Logging Errors and Continuing):在某些情况下,你可能需要在错误发生时记录错误信息,但仍然希望程序继续执行。orElse()
可以用来执行错误日志记录,并返回一个表示“已处理错误”的 Expected
对象。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
int global_retry_count = 0;
8
9
Expected<int, std::string> operation_with_retry() {
10
global_retry_count++;
11
if (global_retry_count <= 2) {
12
std::cout << "Operation failed, retrying... (attempt " << global_retry_count << ")" << std::endl;
13
return makeUnexpected("Transient error"); // 模拟暂时性错误
14
}
15
std::cout << "Operation succeeded after retries." << std::endl;
16
return 100; // 最终成功
17
}
18
19
int main() {
20
auto result = operation_with_retry().orElse([]() {
21
return operation_with_retry(); // 第一次重试
22
}).orElse([]() {
23
return operation_with_retry(); // 第二次重试
24
}).orElse(makeUnexpected("Operation failed after multiple retries")); // 所有重试都失败
25
26
if (result.hasValue()) {
27
std::cout << "Final Result: " << result.value() << std::endl; // 输出: Final Result: 100
28
} else {
29
std::cout << "Final Error: " << result.error() << std::endl;
30
}
31
32
return 0;
33
}
在这个例子中,operation_with_retry
函数模拟一个可能失败的操作,并在前两次调用时返回错误。我们使用链式的 orElse()
调用来重试操作最多两次。如果 operation_with_retry
在三次尝试后仍然失败,最终的 orElse
将返回一个包含错误信息 "Operation failed after multiple retries" 的 Expected
对象。通过这种方式,我们实现了简单的错误重试逻辑。
3.5 transform():通用转换 (General Transformation)
transform()
方法是 Expected
中最通用的转换操作。它允许你同时处理 Expected
对象的值和错误两种状态,并根据不同的状态应用不同的转换函数。transform()
提供了最大的灵活性,可以实现 map()
、mapError()
和 orElse()
等操作的功能,并且可以进行更复杂的转换和组合。
3.5.1 transform() 的灵活性 (Flexibility of transform())
transform()
方法接受一个函数作为参数,这个函数本身又接受一个 Expected
对象作为参数,并返回一个新的 Expected
对象。这个函数的目的是根据输入的 Expected
对象的状态(包含值还是错误),进行相应的转换,并返回转换后的 Expected
对象。
transform()
的函数签名大致如下(简化表示):
1
template <typename F>
2
auto transform(F transform_func) -> decltype(transform_func(std::declval<Expected<T, E>>()));
其中,transform_func
是你提供的转换函数,它接受一个 Expected<T, E>
类型的参数,并返回一个 Expected<U, F>
类型的对象(类型 U
和 F
可以与 T
和 E
相同,也可以不同)。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
using namespace folly;
6
7
Expected<int, std::string> safe_divide(int a, int b) {
8
if (b == 0) {
9
return makeUnexpected("Division by zero");
10
}
11
return a / b;
12
}
13
14
int multiply_by_two(int x) {
15
return x * 2;
16
}
17
18
std::string enhance_error_message(const std::string& error) {
19
return "Error occurred: " + error;
20
}
21
22
int main() {
23
auto result1 = safe_divide(10, 2).transform([](Expected<int, std::string> expected) {
24
if (expected.hasValue()) {
25
return makeExpected(multiply_by_two(expected.value())); // 转换值
26
} else {
27
return makeUnexpected(enhance_error_message(expected.error())); // 转换错误
28
}
29
});
30
31
if (result1.hasValue()) {
32
std::cout << "Result 1: " << result1.value() << std::endl; // 输出: Result 1: 10
33
} else {
34
std::cout << "Error 1: " << result1.error() << std::endl;
35
}
36
37
auto result2 = safe_divide(5, 0).transform([](Expected<int, std::string> expected) {
38
if (expected.hasValue()) {
39
return makeExpected(multiply_by_two(expected.value()));
40
} else {
41
return makeUnexpected(enhance_error_message(expected.error()));
42
}
43
});
44
45
if (result2.hasValue()) {
46
std::cout << "Result 2: " << result2.value() << std::endl;
47
} else {
48
std::cout << "Error 2: " << result2.error() << std::endl; // 输出: Error 2: Error occurred: Division by zero
49
}
50
51
return 0;
52
}
在这个例子中,transform()
接受一个 lambda 函数,该函数检查输入的 Expected
对象的状态:
⚝ 如果 Expected
包含值,lambda 函数将值取出,应用 multiply_by_two
函数进行转换,并返回一个新的包含转换后值的 Expected
对象。
⚝ 如果 Expected
包含错误,lambda 函数将错误信息取出,应用 enhance_error_message
函数进行增强,并返回一个新的包含增强后错误信息的 Expected
对象。
通过 transform()
,我们可以在一个操作中同时处理值和错误两种情况,实现更复杂的转换逻辑。
3.5.2 transform() 的应用示例 (Examples of transform())
transform()
的灵活性使得它在多种场景下都非常有用。
① 实现 map()
和 mapError()
:transform()
可以用来实现 map()
和 mapError()
的功能。
⚝ 实现 map()
: 只转换值,保持错误不变。
1
auto mapped_expected = original_expected.transform([](Expected<T, E> expected) {
2
if (expected.hasValue()) {
3
return makeExpected(value_transform_func(expected.value()));
4
} else {
5
return makeUnexpected(expected.error()); // 保持错误不变
6
}
7
});
⚝ 实现 mapError()
: 只转换错误,保持值不变。
1
auto mapped_error_expected = original_expected.transform([](Expected<T, E> expected) {
2
if (expected.hasValue()) {
3
return makeExpected(expected.value()); // 保持值不变
4
} else {
5
return makeUnexpected(error_transform_func(expected.error()));
6
}
7
});
② 条件性转换 (Conditional Transformation):transform()
可以根据 Expected
对象的状态,执行不同的转换逻辑。例如,只有当值满足特定条件时才进行转换,否则返回一个特定的错误。
③ 组合值和错误信息 (Combining Value and Error Information):transform()
可以用来将值和错误信息组合成一个新的结果类型。例如,将 Expected<int, std::string>
转换为 Expected<std::pair<int, std::string>, std::string>
,其中 pair 的第一个元素是值(如果存在),第二个元素是原始的错误信息(如果存在)。
④ 实现更复杂的控制流 (Implementing More Complex Control Flow):结合 transform()
和其他 Expected
操作,可以实现更复杂的控制流逻辑,例如,根据错误类型选择不同的错误处理策略,或者在值转换过程中引入副作用(例如日志记录)。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
#include <utility>
5
6
using namespace folly;
7
8
Expected<int, std::string> safe_divide(int a, int b) {
9
if (b == 0) {
10
return makeUnexpected("Division by zero");
11
}
12
return a / b;
13
}
14
15
int main() {
16
auto result = safe_divide(10, 2).transform([](Expected<int, std::string> expected) {
17
if (expected.hasValue()) {
18
return makeExpected(std::make_pair(expected.value(), "")); // 成功时,pair 的第二个元素为空字符串
19
} else {
20
return makeUnexpected(std::make_pair(-1, expected.error())); // 失败时,pair 的第一个元素为 -1
21
}
22
});
23
24
if (result.hasValue()) {
25
std::cout << "Result Value: " << result.value().first << ", Error Message: " << result.value().second << std::endl; // 输出: Result Value: 5, Error Message:
26
} else {
27
std::cout << "Error Value Placeholder: " << result.error().first << ", Error Message: " << result.error().second << std::endl; // 输出: Error Value Placeholder: -1, Error Message: Division by zero
28
}
29
30
return 0;
31
}
在这个例子中,transform()
将 Expected<int, std::string>
转换为 Expected<std::pair<int, std::string>, std::pair<int, std::string>>
。当 safe_divide
成功时,pair 的第一个元素是计算结果,第二个元素是空字符串。当 safe_divide
失败时,pair 的第一个元素是一个占位符值 -1
,第二个元素是原始的错误信息。这展示了 transform()
如何用于组合值和错误信息,形成更丰富的结果类型。
END_OF_CHAPTER
4. chapter 4:高级应用与实战案例 (Advanced Applications and Practical Cases)
4.1 自定义错误类型 (Custom Error Types)
在 folly::Expected
的使用中,错误类型(Error Type)的选择至关重要。虽然你可以使用简单的类型如 std::string
或 int
来表示错误,但在更复杂的应用场景中,自定义错误类型能够提供更丰富的信息和更强的类型安全性。自定义错误类型允许你根据具体的业务逻辑和错误场景,设计出更精确、更易于理解和处理的错误信息。本节将深入探讨如何使用枚举(Enums)和类(Classes)来定义自定义错误类型,并介绍错误类型的继承与多态在 Expected
中的应用。
4.1.1 使用枚举 (Enums) 定义错误类型
枚举(Enum)是定义一组具名常量的有效方式,特别适合表示一组预定义的、离散的错误状态。当错误类型可以被清晰地枚举出来,并且错误处理逻辑可以基于这些枚举值进行分支判断时,使用枚举作为 Expected
的错误类型是一个简洁且高效的选择。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
5
// 定义错误枚举
6
enum class FileError {
7
NotFound,
8
PermissionDenied,
9
DiskFull,
10
Unknown
11
};
12
13
folly::Expected<std::string, FileError> readFileContent(const std::string& filename) {
14
if (filename == "important.txt") {
15
return "Content of important.txt"; // 假设文件读取成功
16
} else if (filename == "secret.txt") {
17
return folly::makeUnexpected(FileError::PermissionDenied);
18
} else if (filename == "full.txt") {
19
return folly::makeUnexpected(FileError::DiskFull);
20
} else {
21
return folly::makeUnexpected(FileError::NotFound);
22
}
23
}
24
25
int main() {
26
auto content1 = readFileContent("important.txt");
27
if (content1.hasValue()) {
28
std::cout << "File content: " << content1.value() << std::endl;
29
} else {
30
std::cout << "Error reading file: ";
31
switch (content1.error()) {
32
case FileError::NotFound:
33
std::cout << "File not found." << std::endl;
34
break;
35
case FileError::PermissionDenied:
36
std::cout << "Permission denied." << std::endl;
37
break;
38
case FileError::DiskFull:
39
std::cout << "Disk full." << std::endl;
40
break;
41
case FileError::Unknown:
42
default:
43
std::cout << "Unknown error." << std::endl;
44
break;
45
}
46
}
47
48
auto content2 = readFileContent("nonexistent.txt");
49
if (content2.hasValue()) {
50
std::cout << "File content: " << content2.value() << std::endl;
51
} else {
52
std::cout << "Error reading file: ";
53
switch (content2.error()) {
54
case FileError::NotFound:
55
std::cout << "File not found." << std::endl;
56
break;
57
// ... 其他错误处理
58
default:
59
std::cout << "Unknown error." << std::endl;
60
break;
61
}
62
}
63
64
return 0;
65
}
在这个例子中,FileError
枚举定义了文件操作可能出现的几种错误状态。readFileContent
函数返回一个 folly::Expected<std::string, FileError>
,明确表示函数可能返回文件内容(std::string
)或一个 FileError
枚举值。在 main
函数中,我们通过 hasValue()
和 error()
方法检查 Expected
对象的状态,并根据不同的枚举值进行相应的错误处理。
使用枚举作为错误类型的优点包括:
① 类型安全:枚举是强类型,避免了使用魔术数字或字符串表示错误码可能导致的类型错误。
② 可读性强:枚举名称本身就具有描述性,提高了代码的可读性和可维护性。
③ 易于扩展:可以方便地向枚举中添加新的错误类型,而不会影响现有的错误处理逻辑(在不改变现有枚举值的情况下)。
然而,枚举的局限性在于它只能表示离散的、预定义的错误状态,无法携带更详细的错误信息,例如错误发生的具体位置、上下文信息等。对于需要更丰富错误信息的场景,类可能是一个更合适的选择。
4.1.2 使用类 (Classes) 定义错误类型
当错误需要携带更多上下文信息,或者错误类型之间存在复杂的关联关系时,使用类(Class)来定义错误类型会更加灵活和强大。类可以包含成员变量来存储错误相关的详细信息,例如错误代码、错误消息、错误发生的文件名、行号等。此外,类还可以定义成员函数来提供错误信息的格式化输出、错误类型的判断等功能。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
#include <sstream>
5
6
// 自定义错误类
7
class FileErrorClass {
8
public:
9
enum ErrorCode {
10
NotFound,
11
PermissionDenied,
12
DiskFull,
13
Unknown
14
};
15
16
FileErrorClass(ErrorCode code, const std::string& message, const std::string& filename = "")
17
: errorCode_(code), message_(message), filename_(filename) {}
18
19
ErrorCode getErrorCode() const { return errorCode_; }
20
const std::string& getMessage() const { return message_; }
21
const std::string& getFilename() const { return filename_; }
22
23
std::string formatError() const {
24
std::stringstream ss;
25
ss << "File error: ";
26
switch (errorCode_) {
27
case ErrorCode::NotFound: ss << "Not found"; break;
28
case ErrorCode::PermissionDenied: ss << "Permission denied"; break;
29
case ErrorCode::DiskFull: ss << "Disk full"; break;
30
case ErrorCode::Unknown: ss << "Unknown"; break;
31
}
32
if (!filename_.empty()) {
33
ss << " in file: " << filename_;
34
}
35
ss << ". Message: " << message_;
36
return ss.str();
37
}
38
39
private:
40
ErrorCode errorCode_;
41
std::string message_;
42
std::string filename_;
43
};
44
45
folly::Expected<std::string, FileErrorClass> readFileContentClass(const std::string& filename) {
46
if (filename == "important.txt") {
47
return "Content of important.txt";
48
} else if (filename == "secret.txt") {
49
return folly::makeUnexpected(FileErrorClass(FileErrorClass::ErrorCode::PermissionDenied, "Insufficient permissions to access file", filename));
50
} else if (filename == "full.txt") {
51
return folly::makeUnexpected(FileErrorClass(FileErrorClass::ErrorCode::DiskFull, "Disk is full", filename));
52
} else {
53
return folly::makeUnexpected(FileErrorClass(FileErrorClass::ErrorCode::NotFound, "File not found", filename));
54
}
55
}
56
57
int main() {
58
auto content1 = readFileContentClass("important.txt");
59
if (content1.hasValue()) {
60
std::cout << "File content: " << content1.value() << std::endl;
61
} else {
62
std::cout << content1.error().formatError() << std::endl;
63
}
64
65
auto content2 = readFileContentClass("secret.txt");
66
if (content2.hasValue()) {
67
std::cout << "File content: " << content2.value() << std::endl;
68
} else {
69
std::cout << content2.error().formatError() << std::endl;
70
}
71
72
return 0;
73
}
在这个例子中,FileErrorClass
是一个自定义的错误类,它包含了错误代码(ErrorCode
枚举)、错误消息和文件名。readFileContentClass
函数返回 folly::Expected<std::string, FileErrorClass>
,当发生错误时,会返回一个包含详细错误信息的 FileErrorClass
对象。在 main
函数中,我们通过调用 error().formatError()
方法,可以获取格式化后的错误信息,这比简单的枚举值错误处理更加详细和用户友好。
使用类作为错误类型的优点包括:
① 信息丰富:类可以携带更丰富的错误上下文信息,例如错误代码、错误消息、发生位置等。
② 灵活性高:可以根据需要自定义错误类的成员变量和成员函数,提供更强大的错误处理能力。
③ 支持继承与多态:可以使用继承和多态来构建更复杂的错误类型体系,实现更灵活的错误处理策略。
4.1.3 错误类型的继承与多态 (Inheritance and Polymorphism of Error Types)
当应用程序变得复杂时,错误类型也可能变得多样化。使用继承(Inheritance)和多态(Polymorphism)可以构建一个层次化的错误类型体系,使得错误处理更加模块化和可扩展。例如,我们可以定义一个基类 Error
,表示通用的错误类型,然后派生出具体的错误类型,如 FileError
、NetworkError
、DatabaseError
等。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <string>
4
#include <sstream>
5
6
// 错误基类
7
class Error {
8
public:
9
enum ErrorCode {
10
Unknown
11
};
12
13
Error(ErrorCode code, const std::string& message) : errorCode_(code), message_(message) {}
14
virtual ~Error() = default;
15
16
ErrorCode getErrorCode() const { return errorCode_; }
17
const std::string& getMessage() const { return message_; }
18
19
virtual std::string formatError() const {
20
std::stringstream ss;
21
ss << "Error: Unknown. Message: " << message_;
22
return ss.str();
23
}
24
25
private:
26
ErrorCode errorCode_;
27
std::string message_;
28
};
29
30
// 文件错误类,继承自 Error
31
class FileError : public Error {
32
public:
33
enum FileErrorCode {
34
NotFound,
35
PermissionDenied,
36
DiskFull
37
};
38
39
FileError(FileErrorCode code, const std::string& message, const std::string& filename = "")
40
: Error(ErrorCode::Unknown, message), fileErrorCode_(code), filename_(filename) {}
41
42
FileErrorCode getFileErrorCode() const { return fileErrorCode_; }
43
const std::string& getFilename() const { return filename_; }
44
45
std::string formatError() const override {
46
std::stringstream ss;
47
ss << "File error: ";
48
switch (fileErrorCode_) {
49
case FileErrorCode::NotFound: ss << "Not found"; break;
50
case FileErrorCode::PermissionDenied: ss << "Permission denied"; break;
51
case FileErrorCode::DiskFull: ss << "Disk full"; break;
52
}
53
if (!filename_.empty()) {
54
ss << " in file: " << filename_;
55
}
56
ss << ". Message: " << getMessage(); // 调用基类的 getMessage()
57
return ss.str();
58
}
59
60
private:
61
FileErrorCode fileErrorCode_;
62
std::string filename_;
63
};
64
65
folly::Expected<std::string, std::unique_ptr<Error>> readFileContentHierarchy(const std::string& filename) {
66
if (filename == "important.txt") {
67
return "Content of important.txt";
68
} else if (filename == "secret.txt") {
69
return folly::makeUnexpected(std::make_unique<FileError>(FileError::FileErrorCode::PermissionDenied, "Insufficient permissions", filename));
70
} else {
71
return folly::makeUnexpected(std::make_unique<FileError>(FileError::FileErrorCode::NotFound, "File not found", filename));
72
}
73
}
74
75
void processError(const Error& error) {
76
std::cout << "Processing error: " << error.formatError() << std::endl;
77
// 可以根据 error 的具体类型进行不同的处理
78
if (dynamic_cast<const FileError*>(&error)) {
79
std::cout << "It's a FileError." << std::endl;
80
}
81
}
82
83
int main() {
84
auto content1 = readFileContentHierarchy("important.txt");
85
if (content1.hasValue()) {
86
std::cout << "File content: " << content1.value() << std::endl;
87
} else {
88
processError(*content1.error()); // 传递 Error 对象的引用
89
}
90
91
auto content2 = readFileContentHierarchy("secret.txt");
92
if (content2.hasValue()) {
93
std::cout << "File content: " << content2.value() << std::endl;
94
} else {
95
processError(*content2.error()); // 传递 Error 对象的引用
96
}
97
98
return 0;
99
}
在这个例子中,Error
是一个基类,FileError
继承自 Error
。readFileContentHierarchy
函数返回 folly::Expected<std::string, std::unique_ptr<Error>>
,错误类型是指向 Error
基类的智能指针。在 main
函数中,processError
函数接受一个 Error
对象的引用,并使用多态的方式调用 formatError()
方法,实现了对不同错误类型的统一处理。同时,可以使用 dynamic_cast
进行运行时类型检查,以便针对特定类型的错误进行特殊处理。
使用错误类型的继承与多态的优点包括:
① 代码复用:基类可以定义通用的错误处理逻辑,派生类可以专注于特定类型的错误信息和处理。
② 可扩展性:可以方便地添加新的错误类型,而无需修改现有的错误处理框架。
③ 灵活性:可以使用多态性实现对不同错误类型的统一处理,也可以针对特定错误类型进行特殊处理。
在选择自定义错误类型时,需要根据具体的应用场景和需求进行权衡。对于简单的错误状态,枚举可能足够简洁高效;对于需要携带更多信息或构建复杂错误体系的场景,类和继承则提供了更强大的表达能力和灵活性。
4.2 Expected 与资源管理 (Resource Management)
资源管理是 C++ 编程中一个至关重要的方面。不当的资源管理容易导致资源泄漏、程序崩溃等问题。RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种 C++ 中常用的资源管理技术,它通过将资源的生命周期与对象的生命周期绑定,确保资源在不再需要时能够被自动释放。folly::Expected
可以与 RAII 良好地结合,帮助我们更好地管理程序中的资源,尤其是在可能发生错误的情况下。
4.2.1 RAII 与 Expected (RAII and Expected)
RAII 的核心思想是在对象构造时获取资源,在对象析构时释放资源。C++ 中的智能指针(如 std::unique_ptr
、std::shared_ptr
)是 RAII 的典型应用。当与 Expected
结合使用时,RAII 可以确保即使在函数返回错误的情况下,已经获取的资源也能够被正确释放。
考虑一个文件操作的例子,我们需要打开文件、读取内容,然后在操作完成后关闭文件。使用 RAII,我们可以创建一个文件句柄类,在构造函数中打开文件,在析构函数中关闭文件。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <fstream>
4
#include <string>
5
6
class FileHandle {
7
public:
8
FileHandle(const std::string& filename) : file_(filename) {
9
if (!file_.is_open()) {
10
throw std::runtime_error("Failed to open file: " + filename);
11
}
12
}
13
14
~FileHandle() {
15
if (file_.is_open()) {
16
file_.close();
17
std::cout << "File closed." << std::endl;
18
}
19
}
20
21
std::ifstream& getFileStream() { return file_; }
22
23
private:
24
std::ifstream file_;
25
26
// 禁用拷贝构造和拷贝赋值
27
FileHandle(const FileHandle&) = delete;
28
FileHandle& operator=(const FileHandle&) = delete;
29
};
30
31
folly::Expected<std::string, std::string> readFileContentRAII(const std::string& filename) {
32
try {
33
FileHandle fileHandle(filename); // RAII: 文件在 FileHandle 对象构造时打开
34
std::stringstream buffer;
35
buffer << fileHandle.getFileStream().rdbuf();
36
return buffer.str(); // 文件内容读取成功
37
} catch (const std::exception& e) {
38
return folly::makeUnexpected(e.what()); // 捕获异常,返回错误
39
} // RAII: FileHandle 对象析构时文件自动关闭
40
}
41
42
int main() {
43
auto content1 = readFileContentRAII("example.txt"); // 假设 example.txt 存在
44
if (content1.hasValue()) {
45
std::cout << "File content:\n" << content1.value() << std::endl;
46
} else {
47
std::cerr << "Error reading file: " << content1.error() << std::endl;
48
}
49
50
auto content2 = readFileContentRAII("nonexistent.txt"); // 假设 nonexistent.txt 不存在
51
if (content2.hasValue()) {
52
std::cout << "File content:\n" << content2.value() << std::endl;
53
} else {
54
std::cerr << "Error reading file: " << content2.error() << std::endl;
55
}
56
57
return 0;
58
}
在这个例子中,FileHandle
类实现了 RAII。在 readFileContentRAII
函数中,FileHandle
对象在 try
块中创建,当函数正常返回或抛出异常时,FileHandle
对象都会被析构,从而确保文件被正确关闭。folly::Expected
用于包装函数的结果,可以是文件内容(成功)或错误信息(失败)。即使 readFileContentRAII
函数返回一个 unexpected 的 Expected
,FileHandle
的析构函数仍然会被调用,资源得到释放。
4.2.2 在 Expected 中管理资源生命周期 (Managing Resource Lifecycles in Expected)
除了使用 RAII 类来管理资源,我们还可以直接在 Expected
中管理资源的生命周期,尤其是在函数需要返回资源的所有权时。例如,考虑一个工厂函数,它根据输入参数创建某种资源,并将资源的所有权转移给调用者。使用 Expected
,我们可以清晰地表达工厂函数可能创建资源成功或失败,并将资源的所有权安全地传递出去。
1
#include <folly/Expected.h>
2
#include <iostream>
3
#include <memory>
4
5
class Resource {
6
public:
7
Resource(int id) : id_(id) {
8
std::cout << "Resource " << id_ << " acquired." << std::endl;
9
}
10
~Resource() {
11
std::cout << "Resource " << id_ << " released." << std::endl;
12
}
13
14
void useResource() const {
15
std::cout << "Using resource " << id_ << std::endl;
16
}
17
18
private:
19
int id_;
20
};
21
22
enum class ResourceError {
23
OutOfResources,
24
InvalidParameter
25
};
26
27
folly::Expected<std::unique_ptr<Resource>, ResourceError> createResource(int id) {
28
if (id >= 0) {
29
return std::make_unique<Resource>(id); // 成功创建资源,返回 unique_ptr
30
} else {
31
return folly::makeUnexpected(ResourceError::InvalidParameter); // 参数无效,创建失败
32
}
33
}
34
35
int main() {
36
auto resource1 = createResource(1);
37
if (resource1.hasValue()) {
38
resource1.value()->useResource(); // 使用资源
39
} else {
40
std::cerr << "Failed to create resource: ";
41
switch (resource1.error()) {
42
case ResourceError::OutOfResources:
43
std::cerr << "Out of resources." << std::endl;
44
break;
45
case ResourceError::InvalidParameter:
46
std::cerr << "Invalid parameter." << std::endl;
47
break;
48
}
49
} // resource1 (unique_ptr) 在作用域结束时自动释放资源
50
51
auto resource2 = createResource(-1);
52
if (resource2.hasValue()) {
53
resource2.value()->useResource();
54
} else {
55
std::cerr << "Failed to create resource: ";
56
switch (resource2.error()) {
57
case ResourceError::OutOfResources:
58
std::cerr << "Out of resources." << std::endl;
59
break;
60
case ResourceError::InvalidParameter:
61
std::cerr << "Invalid parameter." << std::endl;
62
break;
63
}
64
} // resource2 (unexpected Expected) 作用域结束,但没有资源需要释放
65
66
return 0;
67
}
在这个例子中,createResource
函数尝试创建一个 Resource
对象,并返回一个 folly::Expected<std::unique_ptr<Resource>, ResourceError>
。如果资源创建成功,Expected
包含一个 std::unique_ptr<Resource>
,资源的所有权被转移给调用者。如果创建失败,Expected
包含一个 ResourceError
枚举值。由于 Expected
存储的是 std::unique_ptr
,当 Expected
对象超出作用域时,即使是 unexpected 状态,unique_ptr
也会被析构(如果存在),从而确保资源被正确释放。
通过结合 Expected
和 RAII,我们可以编写出更安全、更健壮的代码,有效地管理程序中的资源,并清晰地处理可能出现的错误情况。
4.3 Expected 在并发编程中的应用 (Expected in Concurrent Programming)
并发编程是现代软件开发中不可或缺的一部分。在并发环境中,错误处理变得更加复杂,因为错误可能发生在不同的线程中,需要一种机制来安全地传递和处理错误。folly::Expected
在并发编程中也发挥着重要作用,尤其是在处理异步操作的结果时。
4.3.1 线程安全 (Thread Safety)
folly::Expected
本身的设计是线程安全的,这意味着多个线程可以同时访问和操作不同的 Expected
对象,而不会发生数据竞争等问题。但是,Expected
对象内部存储的值类型(Value Type)和错误类型(Error Type)的线程安全性取决于这些类型自身的实现。如果 Value Type 或 Error Type 不是线程安全的,那么在并发环境中使用 Expected
时,仍然需要注意线程安全问题。
对于基本类型(如 int
、bool
、指针等)和标准库中的线程安全类型(如 std::atomic<int>
、std::mutex
等),可以直接在 Expected
中使用,而无需额外的同步措施。对于自定义的类类型,需要确保其成员变量的访问和修改是线程安全的,或者使用适当的同步机制(如互斥锁、原子操作)来保护共享状态。
在并发编程中使用 Expected
时,一个常见的场景是在不同的线程之间传递操作结果。由于 Expected
是可移动的(Moveable),可以安全地将 Expected
对象从一个线程移动到另一个线程,从而传递操作的结果(包括成功的值或错误信息)。
4.3.2 异步操作与 Expected (Asynchronous Operations and Expected)
在异步编程中,操作的结果通常在未来的某个时间点才能得到。folly::Future
是 Folly 库中用于表示异步操作结果的类。Future
可以与 Expected
结合使用,表示异步操作可能成功返回一个值,也可能失败返回一个错误。
1
#include <folly/Expected.h>
2
#include <folly/Future.h>
3
#include <folly/executors/InlineExecutor.h>
4
#include <iostream>
5
#include <string>
6
7
using namespace folly;
8
9
enum class AsyncError {
10
Timeout,
11
NetworkError,
12
ServerError
13
};
14
15
Future<Expected<std::string, AsyncError>> fetchDataAsync() {
16
// 模拟异步操作,例如网络请求
17
// 这里为了简化,直接返回一个 Future,实际应用中可能是网络库的异步接口
18
return makeFuture(Expected<std::string, AsyncError>("Data from server")); // 假设操作成功
19
// 或者返回 makeFuture(Expected<std::string, AsyncError>(makeUnexpected(AsyncError::NetworkError))); // 假设操作失败
20
}
21
22
int main() {
23
auto futureResult = fetchDataAsync();
24
25
// 使用 then() 链式处理异步操作的结果
26
futureResult.then([](Expected<std::string, AsyncError> result) {
27
if (result.hasValue()) {
28
std::cout << "Async operation succeeded. Data: " << result.value() << std::endl;
29
} else {
30
std::cerr << "Async operation failed. Error: ";
31
switch (result.error()) {
32
case AsyncError::Timeout:
33
std::cerr << "Timeout." << std::endl;
34
break;
35
case AsyncError::NetworkError:
36
std::cerr << "Network error." << std::endl;
37
break;
38
case AsyncError::ServerError:
39
std::cerr << "Server error." << std::endl;
40
break;
41
}
42
}
43
}).get(); // 使用 get() 阻塞等待 Future 完成 (仅为示例,实际异步编程中应避免阻塞主线程)
44
45
return 0;
46
}
在这个例子中,fetchDataAsync
函数返回一个 Future<Expected<std::string, AsyncError>>
,表示异步获取数据的操作。Future
包装了 Expected
,使得我们可以使用 Future
的链式操作(如 then()
)来处理异步操作的结果,无论是成功的值还是错误。在 then()
回调函数中,我们可以像处理普通的 Expected
对象一样,检查操作是否成功,并访问值或错误。
通过结合 Future
和 Expected
,我们可以构建更清晰、更易于管理的异步错误处理流程。Future
负责处理异步操作的调度和结果传递,Expected
负责清晰地表达操作的结果是成功还是失败,以及失败时的错误信息。这种组合使得异步代码更易于理解、测试和维护。
4.4 性能考量与优化 (Performance Considerations and Optimization)
虽然 folly::Expected
提供了很多优点,例如清晰的错误处理、函数式操作等,但在性能敏感的场景中,我们需要考虑其性能开销,并采取一些优化措施。
4.4.1 Expected 的性能开销 (Performance Overhead of Expected)
folly::Expected
的主要性能开销来自于以下几个方面:
① 额外的存储空间:Expected
需要额外的空间来存储状态标志(表示是包含值还是错误)以及可能的值或错误对象。这通常比直接返回一个值或抛出异常的开销要大。
② 构造和析构开销:创建和销毁 Expected
对象本身会有一定的开销,特别是当 Value Type 或 Error Type 的构造和析构函数比较复杂时。
③ 间接访问:访问 Expected
中存储的值或错误需要通过额外的函数调用(如 value()
、error()
),这会引入一定的间接访问开销。
④ 函数式操作的开销:使用 map()
、andThen()
等函数式操作会引入额外的函数调用和对象创建开销。
在大多数应用场景中,Expected
的性能开销通常是可以接受的,甚至可以忽略不计,尤其是在与异常处理的开销相比时。然而,在极端的性能敏感场景,例如循环内部、频繁调用的底层库函数等,我们需要仔细评估 Expected
的性能影响。
4.4.2 避免不必要的拷贝 (Avoiding Unnecessary Copies)
为了减少性能开销,一个重要的优化策略是避免不必要的拷贝。folly::Expected
自身的设计已经尽可能地减少了拷贝操作。例如,Expected
支持移动语义(Move Semantics),可以使用移动构造函数和移动赋值运算符来高效地转移资源的所有权。
在使用 Expected
时,我们应该尽量使用移动操作,而不是拷贝操作。例如,在函数返回 Expected
对象时,应该使用值返回(Return by Value),让编译器进行返回值优化(Return Value Optimization,RVO)或移动操作。
1
folly::Expected<std::string, std::string> processData() {
2
std::string data = "large data string"; // 假设 data 是一个大型字符串
3
// ... 对 data 进行处理 ...
4
return data; // 值返回,编译器会尝试 RVO 或移动操作
5
}
6
7
int main() {
8
auto result = processData(); // 接收返回值,避免不必要的拷贝
9
if (result.hasValue()) {
10
std::cout << "Result: " << result.value() << std::endl;
11
}
12
return 0;
13
}
另外,在使用 map()
、andThen()
等函数式操作时,也应该注意避免不必要的拷贝。例如,如果转换函数返回的对象比较大,可以考虑返回移动语义的对象,或者使用 std::reference_wrapper
来避免拷贝。
4.4.3 选择合适的错误类型 (Choosing Appropriate Error Types)
错误类型的选择也会影响 Expected
的性能。选择简单的、轻量级的错误类型可以减少性能开销。例如,如果只需要表示几种预定义的错误状态,使用枚举类型通常比使用复杂的类类型更高效。
如果错误类型需要携带大量的信息,但这些信息不是总是需要的,可以考虑使用延迟加载(Lazy Loading)或按需构造(On-demand Construction)的方式来优化。例如,错误信息可以只在需要时才生成,而不是在 Expected
对象创建时就立即生成。
在性能敏感的场景中,还可以考虑使用更底层的错误处理机制,例如错误码或异常,而不是 Expected
。但是,这样做可能会牺牲代码的可读性和可维护性。因此,在进行性能优化时,需要在性能、可读性、可维护性之间进行权衡。
总的来说,folly::Expected
的性能开销在大多数情况下是可以接受的。通过避免不必要的拷贝、选择合适的错误类型等优化措施,可以进一步减少性能开销,使得 Expected
能够在性能敏感的场景中也能发挥作用。
4.5 实战案例:构建健壮的 API (Practical Case: Building Robust APIs)
构建健壮的 API(Application Programming Interface,应用程序编程接口)是软件开发中的一项重要任务。API 的健壮性直接影响到系统的稳定性、可靠性和可维护性。folly::Expected
可以帮助我们设计出更健壮的 API,清晰地表达 API 的返回值和可能出现的错误,提高 API 的可用性和易用性。
4.5.1 使用 Expected 设计 API 接口 (Designing API Interfaces with Expected)
在设计 API 接口时,使用 folly::Expected
可以明确地指示 API 函数可能返回成功的值或错误。这使得 API 的使用者能够清晰地了解 API 的行为,并编写相应的错误处理代码。
例如,考虑一个用户认证 API,authenticateUser
函数接受用户名和密码,验证成功后返回用户 ID,验证失败则返回错误信息。使用 Expected
,我们可以将 API 接口设计如下:
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
enum class AuthError {
6
InvalidUsername,
7
InvalidPassword,
8
AccountLocked,
9
ServerError
10
};
11
12
folly::Expected<int, AuthError> authenticateUser(const std::string& username, const std::string& password) {
13
if (username == "testuser") {
14
if (password == "password123") {
15
return 12345; // 假设用户 ID 为 12345
16
} else {
17
return folly::makeUnexpected(AuthError::InvalidPassword);
18
}
19
} else if (username == "lockeduser") {
20
return folly::makeUnexpected(AuthError::AccountLocked);
21
} else {
22
return folly::makeUnexpected(AuthError::InvalidUsername);
23
}
24
}
25
26
int main() {
27
auto result1 = authenticateUser("testuser", "password123");
28
if (result1.hasValue()) {
29
std::cout << "Authentication successful. User ID: " << result1.value() << std::endl;
30
} else {
31
std::cerr << "Authentication failed. Error: ";
32
switch (result1.error()) {
33
case AuthError::InvalidUsername:
34
std::cerr << "Invalid username." << std::endl;
35
break;
36
case AuthError::InvalidPassword:
37
std::cerr << "Invalid password." << std::endl;
38
break;
39
case AuthError::AccountLocked:
40
std::cerr << "Account locked." << std::endl;
41
break;
42
case AuthError::ServerError:
43
std::cerr << "Server error." << std::endl;
44
break;
45
}
46
}
47
48
auto result2 = authenticateUser("invaliduser", "wrongpassword");
49
if (result2.hasValue()) {
50
std::cout << "Authentication successful. User ID: " << result2.value() << std::endl;
51
} else {
52
std::cerr << "Authentication failed. Error: ";
53
switch (result2.error()) {
54
case AuthError::InvalidUsername:
55
std::cerr << "Invalid username." << std::endl;
56
break;
57
// ... 其他错误处理
58
}
59
}
60
61
return 0;
62
}
在这个例子中,authenticateUser
函数返回 folly::Expected<int, AuthError>
,明确表示函数可能返回用户 ID(int
)或一个 AuthError
枚举值。API 的使用者可以通过检查 Expected
对象的状态,来判断认证是否成功,并根据不同的错误类型进行相应的处理。
使用 Expected
设计 API 接口的优点包括:
① 清晰的返回值类型:Expected
明确地表示 API 函数可能返回的值类型和错误类型,提高了 API 的可读性和可理解性。
② 强制错误处理:API 的使用者必须显式地处理 Expected
返回的错误,避免了忽略错误的可能性。
③ 易于链式调用:Expected
支持函数式操作,可以方便地进行 API 调用的链式组合和错误处理。
4.5.2 错误码与错误信息的设计 (Design of Error Codes and Error Messages)
在 API 设计中,错误码(Error Codes)和错误信息(Error Messages)的设计至关重要。错误码用于程序化地判断错误类型,错误信息用于向用户或开发者提供更详细的错误描述。
当使用 Expected
设计 API 时,错误类型(Error Type)可以作为错误码使用。我们可以使用枚举或类来定义错误类型,并为每个错误类型分配一个唯一的错误码。错误信息可以作为错误类型的成员变量或通过格式化函数提供。
在设计错误码和错误信息时,需要考虑以下几点:
① 一致性:API 中所有函数的错误码和错误信息应该保持一致的风格和格式。
② 可读性:错误码和错误信息应该易于理解和解释,方便 API 的使用者进行错误处理和调试。
③ 详细程度:错误信息应该提供足够的信息,帮助 API 的使用者定位和解决问题。
④ 国际化:如果 API 需要支持多语言,错误信息应该考虑国际化和本地化。
在上面的 authenticateUser
例子中,AuthError
枚举就是一组错误码,枚举名称本身就具有一定的描述性。如果需要更详细的错误信息,可以将 AuthError
定义为类,并添加错误消息成员变量。
4.5.3 API 版本的兼容性 (API Version Compatibility)
API 的版本兼容性是 API 设计中需要考虑的一个重要问题。当 API 需要升级或修改时,如何保证新版本的 API 不会破坏旧版本的 API 的使用者,是一个挑战。
使用 folly::Expected
设计 API,可以更容易地实现 API 的版本兼容性。例如,当需要向 API 添加新的错误类型时,可以在现有的错误枚举或错误类中添加新的枚举值或派生新的错误类,而不会影响旧版本的 API 的使用者。
在处理 API 版本兼容性时,可以考虑以下策略:
① 向后兼容:新版本的 API 应该尽可能地向后兼容旧版本的 API。这意味着新版本的 API 应该能够处理旧版本的 API 的请求,并且旧版本的 API 的使用者可以继续使用新版本的 API,而无需修改代码。
② 版本控制:可以使用版本号来区分不同版本的 API。API 的使用者可以根据需要选择使用特定版本的 API。
③ 错误处理的兼容性:在添加新的错误类型时,应该保证旧版本的 API 的使用者能够正确地处理新的错误类型,例如,将新的错误类型映射到旧版本的通用错误类型。
通过合理地设计错误类型、错误码和错误信息,并采取适当的版本控制策略,我们可以使用 folly::Expected
构建出更健壮、更易于维护、更具有版本兼容性的 API。
END_OF_CHAPTER
5. chapter 5:Expected API 全面解析 (Comprehensive API Analysis)
5.1 Expected 类模板详解 (Detailed Explanation of Expected Class Template)
folly::Expected
是一个类模板,它被设计用来显式地表示一个操作可能成功返回一个值,或者失败并返回一个错误。这种设计使得错误处理更加清晰和类型安全,尤其是在复杂的 C++ 应用中。
5.1.1 模板参数 (Template Parameters)
folly::Expected
类模板接受两个模板参数,分别用于表示成功时的值类型和失败时的错误类型。其模板声明如下:
1
template <typename T, typename E>
2
class Expected;
① typename T
(值类型 Value Type):
这是 Expected
对象在操作成功时所包含的值的类型。T
可以是任何 C++ 类型,包括基本类型、自定义类型、指针、智能指针等。它代表了函数或操作的正常结果。例如,如果一个函数旨在计算两个数的和,那么 T
可以是 int
或 double
。
② typename E
(错误类型 Error Type):
这是 Expected
对象在操作失败时所包含的错误类型。E
同样可以是任何 C++ 类型,但通常建议选择能够提供丰富错误信息的类型,例如枚举类、错误码类、或者包含错误描述的自定义类。选择合适的错误类型对于错误处理和调试至关重要。例如,在一个文件操作的场景中,E
可以是一个枚举,包含如 FileNotFoundError
、PermissionError
等错误类型。
示例:
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
// 定义一个可能失败的函数,返回 Expected<int, std::string>
6
folly::Expected<int, std::string> divide(int numerator, int denominator) {
7
if (denominator == 0) {
8
return folly::makeUnexpected("Division by zero"); // 返回错误
9
}
10
return numerator / denominator; // 返回值
11
}
12
13
int main() {
14
auto result1 = divide(10, 2); // 正常情况
15
if (result1.hasValue()) {
16
std::cout << "Result: " << result1.value() << std::endl; // 访问值
17
}
18
19
auto result2 = divide(5, 0); // 错误情况
20
if (result2.hasError()) {
21
std::cerr << "Error: " << result2.error() << std::endl; // 访问错误
22
}
23
return 0;
24
}
在这个例子中,folly::Expected<int, std::string>
表示函数 divide
可能返回一个 int
值(成功)或一个 std::string
类型的错误信息(失败)。int
是值类型 T
,std::string
是错误类型 E
。
5.1.2 成员类型 (Member Types)
folly::Expected
类模板定义了一些有用的成员类型,这些类型提供了关于 Expected
对象内部类型信息的访问方式,增强了代码的清晰度和可读性。
① value_type
:
定义为模板参数 T
,即值类型。它表示 Expected
成功时所包含的值的类型。
② error_type
:
定义为模板参数 E
,即错误类型。它表示 Expected
失败时所包含的错误的类型。
③ unexpected_type
:
这是一个嵌套的类模板,用于包装错误类型 E
。在内部,folly::Expected
使用 unexpected_type<E>
来存储错误值。虽然用户通常不需要直接操作 unexpected_type
,但了解它的存在有助于理解 Expected
的内部实现。
④ has_value_type
:
类型为 bool
,用于表示 Expected
对象是否包含一个值。这与 hasValue()
成员函数返回的类型一致。
⑤ has_error_type
:
类型为 bool
,用于表示 Expected
对象是否包含一个错误。这与 hasError()
成员函数返回的类型一致。
使用成员类型的示例:
1
#include <folly/Expected.h>
2
#include <type_traits>
3
#include <iostream>
4
5
int main() {
6
folly::Expected<int, std::string> expectedIntString;
7
8
// 使用成员类型来获取值类型和错误类型
9
using ValueType = folly::Expected<int, std::string>::value_type;
10
using ErrorType = folly::Expected<int, std::string>::error_type;
11
12
// 静态断言来验证类型是否正确
13
static_assert(std::is_same_v<ValueType, int>, "ValueType should be int");
14
static_assert(std::is_same_v<ErrorType, std::string>, "ErrorType should be std::string");
15
16
std::cout << "Value Type: " << typeid(ValueType).name() << std::endl;
17
std::cout << "Error Type: " << typeid(ErrorType).name() << std::endl;
18
19
return 0;
20
}
在这个例子中,我们使用 folly::Expected<int, std::string>::value_type
和 folly::Expected<int, std::string>::error_type
来获取 Expected
的值类型和错误类型,并使用 static_assert
进行了编译时检查,确保类型符合预期。这展示了如何通过成员类型在编译时获取和使用 Expected
的类型信息。
5.2 构造函数与析构函数 (Constructors and Destructor)
folly::Expected
提供了多种构造函数,允许开发者以不同的方式创建 Expected
对象,无论是包含一个值,还是包含一个错误。同时,析构函数负责在 Expected
对象生命周期结束时进行清理工作。
5.2.1 各种构造函数的详细说明 (Detailed Description of Various Constructors)
folly::Expected
提供了以下几种主要的构造函数:
① 默认构造函数 (Default Constructor):
Expected() noexcept;
默认构造函数创建一个 未初始化 的 Expected
对象。这意味着它既不包含值,也不包含错误。默认构造的 Expected
对象处于无效状态,不应直接使用 value()
或 error()
访问其内容,除非之后通过赋值操作符赋予有效状态。默认构造函数通常用于需要先声明 Expected
变量,然后再在稍后的代码中赋值的场景。
1
folly::Expected<int, std::string> expectedValue; // 默认构造
2
// ... 稍后赋值
3
expectedValue = 10; // 赋予值
② 值构造函数 (Value Constructor):
Expected(const T& value);
Expected(T&& value);
值构造函数用于创建一个 包含值的 Expected
对象。它接受一个类型为 T
的值,并通过拷贝或移动语义将其存储在 Expected
对象中。这表示操作成功并返回了预期的结果。
1
folly::Expected<int, std::string> expectedValue1(42); // 拷贝构造
2
int value = 100;
3
folly::Expected<int, std::string> expectedValue2(std::move(value)); // 移动构造
③ 错误构造函数 (Error Constructor):
Expected(folly::unexpected_t, const E& error);
Expected(folly::unexpected_t, E&& error);
Expected(folly::unexpected<E> unexpectedError);
错误构造函数用于创建一个 包含错误的 Expected
对象。它通过 folly::unexpected_t
标签或 folly::unexpected<E>
对象来区分于值构造函数,并接受一个类型为 E
的错误值。这表示操作失败,并提供了相应的错误信息。
1
folly::Expected<int, std::string> expectedError1(folly::unexpected, "File not found"); // 拷贝错误
2
std::string errorMsg = "Invalid input";
3
folly::Expected<int, std::string> expectedError2(folly::unexpected, std::move(errorMsg)); // 移动错误
4
folly::Expected<int, std::string> expectedError3(folly::makeUnexpected("Generic error")); // 使用 makeUnexpected
folly::makeUnexpected(error)
是一个辅助函数,用于更方便地创建包含错误的 Expected
对象,它等价于 Expected(folly::unexpected, error)
。
④ 移动构造函数 (Move Constructor):
Expected(Expected&& other) noexcept;
移动构造函数允许从一个 Expected
对象 移动资源到新的 Expected
对象,而避免深拷贝。这在处理包含复杂类型或大型对象的 Expected
时非常高效。移动构造后,源 Expected
对象的状态变为有效但未指定,通常不应再使用。
1
folly::Expected<std::vector<int>, std::string> createExpectedVector() {
2
std::vector<int> vec = {1, 2, 3, 4, 5};
3
return folly::Expected<std::vector<int>, std::string>(std::move(vec)); // 移动构造返回
4
}
5
6
folly::Expected<std::vector<int>, std::string> expectedVec1 = createExpectedVector(); // 移动构造
7
folly::Expected<std::vector<int>, std::string> expectedVec2 = std::move(expectedVec1); // 显式移动构造
⑤ 拷贝构造函数 (Copy Constructor):
Expected(const Expected& other);
拷贝构造函数创建一个新的 Expected
对象,它是现有 Expected
对象的副本。如果原始 Expected
对象包含值,则新对象也包含相同的值(拷贝);如果原始对象包含错误,则新对象也包含相同的错误(拷贝)。拷贝构造确保了 Expected
对象的复制行为符合预期。
1
folly::Expected<int, std::string> originalExpected(123);
2
folly::Expected<int, std::string> copiedExpected = originalExpected; // 拷贝构造
总结:
构造函数类型 | 描述 | 示例 |
---|---|---|
默认构造函数 | 创建未初始化的 Expected 对象 | folly::Expected<int, std::string> expected; |
值构造函数 | 创建包含值的 Expected 对象 | folly::Expected<int, std::string> expected(42); |
错误构造函数 | 创建包含错误的 Expected 对象 | folly::Expected<int, std::string> expected(folly::unexpected, "Error"); |
移动构造函数 | 从另一个 Expected 对象移动资源 | folly::Expected<T, E> moved(std::move(original)); |
拷贝构造函数 | 复制另一个 Expected 对象 | folly::Expected<T, E> copied = original; |
5.2.2 析构函数的行为 (Behavior of Destructor)
~Expected();
folly::Expected
的析构函数负责 清理 Expected
对象所持有的资源。析构函数的行为取决于 Expected
对象当前的状态(包含值或错误)以及值类型 T
和错误类型 E
的析构行为。
① 当 Expected
包含值时:
如果 Expected
对象存储了一个值(即 hasValue()
返回 true
),析构函数会调用值类型 T
的析构函数。这意味着如果 T
是一个类类型,其析构函数会被执行,负责释放值对象所管理的任何资源(例如,动态分配的内存、打开的文件句柄等)。
② 当 Expected
包含错误时:
如果 Expected
对象存储了一个错误(即 hasError()
返回 true
),析构函数会调用错误类型 E
的析构函数。类似于值类型,如果 E
是一个类类型,其析构函数会被执行,负责释放错误对象所管理的任何资源。
③ 无异常抛出 (No Throw):
folly::Expected
的析构函数被声明为 noexcept
,这意味着 析构函数保证不会抛出异常。这是一个重要的特性,符合 C++ 中析构函数不应抛出异常的最佳实践。
示例:
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
struct Resource {
5
Resource() { std::cout << "Resource acquired." << std::endl; }
6
~Resource() { std::cout << "Resource released." << std::endl; }
7
};
8
9
folly::Expected<Resource, std::string> createResourceExpected(bool succeed) {
10
if (succeed) {
11
return Resource{}; // 值构造,Resource 对象被创建
12
} else {
13
return folly::makeUnexpected("Failed to acquire resource"); // 错误构造
14
}
15
}
16
17
int main() {
18
{
19
std::cout << "Creating Expected with value:" << std::endl;
20
auto expectedWithValue = createResourceExpected(true); // 创建包含值的 Expected
21
} // expectedWithValue 离开作用域,析构函数被调用,Resource 的析构函数执行
22
23
{
24
std::cout << "\nCreating Expected with error:" << std::endl;
25
auto expectedWithError = createResourceExpected(false); // 创建包含错误的 Expected
26
} // expectedWithError 离开作用域,析构函数被调用,std::string 的析构函数执行
27
28
std::cout << "\nProgram finished." << std::endl;
29
return 0;
30
}
在这个例子中,Resource
类在构造和析构时会打印消息。当 Expected<Resource, std::string>
对象 expectedWithValue
包含值时,Resource
的析构函数会在 expectedWithValue
离开作用域时被调用,表明资源被正确释放。同样,对于包含错误的 expectedWithError
,虽然没有 Resource
对象需要析构,但 Expected
自身的析构过程仍然正常执行。
5.3 成员函数详解 (Detailed Explanation of Member Functions)
folly::Expected
提供了丰富的成员函数,用于检查状态、访问值或错误、以及进行转换操作。这些成员函数是使用 Expected
进行错误处理和结果操作的核心。
5.3.1 状态检查函数 (Status Checking Functions)
状态检查函数用于查询 Expected
对象当前的状态,即它是否包含一个值或一个错误。
① hasValue() const noexcept
:
返回一个 bool
值,指示 Expected
对象是否包含一个值。如果返回 true
,则可以使用 value()
函数安全地访问值;如果返回 false
,则表示 Expected
对象包含一个错误。
1
folly::Expected<int, std::string> result1 = 100;
2
folly::Expected<int, std::string> result2 = folly::makeUnexpected("Calculation failed");
3
4
if (result1.hasValue()) {
5
std::cout << "result1 has value." << std::endl; // 输出
6
} else {
7
std::cout << "result1 has error." << std::endl;
8
}
9
10
if (result2.hasValue()) {
11
std::cout << "result2 has value." << std::endl;
12
} else {
13
std::cout << "result2 has error." << std::endl; // 输出
14
}
② hasError() const noexcept
:
返回一个 bool
值,指示 Expected
对象是否包含一个错误。如果返回 true
,则可以使用 error()
函数安全地访问错误;如果返回 false
,则表示 Expected
对象包含一个值。hasError()
的行为逻辑上等同于 !hasValue()
。
1
folly::Expected<int, std::string> result1 = 100;
2
folly::Expected<int, std::string> result2 = folly::makeUnexpected("Calculation failed");
3
4
if (result1.hasError()) {
5
std::cout << "result1 has error." << std::endl;
6
} else {
7
std::cout << "result1 has value." << std::endl; // 输出
8
}
9
10
if (result2.hasError()) {
11
std::cout << "result2 has error." << std::endl; // 输出
12
} else {
13
std::cout << "result2 has value." << std::endl;
14
}
5.3.2 值与错误访问函数 (Value and Error Access Functions)
值与错误访问函数用于获取 Expected
对象中存储的值或错误。在调用这些函数之前,务必使用状态检查函数 (hasValue()
或 hasError()
) 确认 Expected
对象的状态,以避免未定义行为。
① value() &
和 value() &&
:
const T& value() const&;
T&& value() &&;
value()
函数用于 访问 Expected
对象中存储的值。
⚝ 如果 Expected
对象包含值(hasValue()
返回 true
),value()
返回对该值的引用(const T&
或 T&&
)。
⚝ 如果 Expected
对象不包含值(hasValue()
返回 false
),调用 value()
将导致未定义行为,通常会抛出异常。因此,必须在调用 value()
之前检查 hasValue()
。
1
folly::Expected<int, std::string> result = 123;
2
if (result.hasValue()) {
3
int val = result.value(); // 访问值
4
std::cout << "Value: " << val << std::endl; // 输出: Value: 123
5
}
6
7
folly::Expected<std::string, std::string> nameResult = "Alice";
8
if (nameResult.hasValue()) {
9
std::string name = std::move(nameResult).value(); // 移动访问值
10
std::cout << "Name: " << name << std::endl; // 输出: Name: Alice
11
}
② error() &
和 error() &&
:
const E& error() const&;
E&& error() &&;
error()
函数用于 访问 Expected
对象中存储的错误。
⚝ 如果 Expected
对象包含错误(hasError()
返回 true
),error()
返回对该错误的引用(const E&
或 E&&
)。
⚝ 如果 Expected
对象不包含错误(hasError()
返回 false
),调用 error()
将导致未定义行为,通常会抛出异常。因此,必须在调用 error()
之前检查 hasError()
。
1
folly::Expected<int, std::string> result = folly::makeUnexpected("Invalid operation");
2
if (result.hasError()) {
3
std::string err = result.error(); // 访问错误
4
std::cerr << "Error: " << err << std::endl; // 输出: Error: Invalid operation
5
}
6
7
folly::Expected<int, std::vector<std::string>> complexErrorResult = folly::makeUnexpected(std::vector<std::string>{"Error code 1", "Error details"});
8
if (complexErrorResult.hasError()) {
9
std::vector<std::string> errors = std::move(complexErrorResult).error(); // 移动访问错误
10
std::cerr << "Errors: " << std::endl;
11
for (const auto& e : errors) {
12
std::cerr << "- " << e << std::endl;
13
}
14
// 输出:
15
// Errors:
16
// - Error code 1
17
// - Error details
18
}
③ valueOr(U&& defaultValue) &&
和 valueOr(const U& defaultValue) &
:
template <typename U> T valueOr(U&& defaultValue) &&;
template <typename U> T valueOr(const U& defaultValue) const&;
valueOr()
函数提供了一种 在 Expected
对象不包含值时返回默认值的方式。
⚝ 如果 Expected
对象包含值(hasValue()
返回 true
),valueOr()
返回存储的值。
⚝ 如果 Expected
对象包含错误(hasError()
返回 false
),valueOr()
返回提供的 defaultValue
。defaultValue
的类型 U
可以隐式转换为值类型 T
。
valueOr()
避免了显式检查 hasValue()
的步骤,并提供了一种更简洁的方式来处理可能缺失值的情况。
1
folly::Expected<int, std::string> result1 = 123;
2
folly::Expected<int, std::string> result2 = folly::makeUnexpected("Not found");
3
4
int val1 = result1.valueOr(-1); // result1 包含值,返回 123
5
int val2 = result2.valueOr(-1); // result2 包含错误,返回默认值 -1
6
7
std::cout << "Value 1: " << val1 << std::endl; // 输出: Value 1: 123
8
std::cout << "Value 2: " << val2 << std::endl; // 输出: Value 2: -1
9
10
folly::Expected<std::string, std::string> nameResult = folly::makeUnexpected("Name unavailable");
11
std::string defaultName = "Guest";
12
std::string name = nameResult.valueOr(defaultName); // 返回默认名字 "Guest"
13
std::cout << "Name: " << name << std::endl; // 输出: Name: Guest
④ errorOr(F&& defaultError) &&
和 errorOr(const F& defaultError) &
:
template <typename F> E errorOr(F&& defaultError) &&;
template <typename F> E errorOr(const F& defaultError) const&;
errorOr()
函数与 valueOr()
类似,但它 在 Expected
对象不包含错误时返回默认错误。
⚝ 如果 Expected
对象包含错误(hasError()
返回 true
),errorOr()
返回存储的错误。
⚝ 如果 Expected
对象包含值(hasValue()
返回 false
),errorOr()
返回提供的 defaultError
。defaultError
的类型 F
可以隐式转换为错误类型 E
。
errorOr()
在需要提供默认错误信息,或者在错误处理链中需要替换错误时非常有用。但通常情况下,Expected
的设计目的是为了处理错误,而不是在没有错误时“制造”一个错误,因此 errorOr()
的使用场景相对较少。
1
folly::Expected<int, std::string> result1 = folly::makeUnexpected("File access denied");
2
folly::Expected<int, std::string> result2 = 456;
3
4
std::string error1 = result1.errorOr("Unknown error"); // result1 包含错误,返回 "File access denied"
5
std::string error2 = result2.errorOr("No error"); // result2 包含值,返回默认错误 "No error"
6
7
std::cerr << "Error 1: " << error1 << std::endl; // 输出: Error 1: File access denied
8
std::cerr << "Error 2: " << error2 << std::endl; // 输出: Error 2: No error
注意: 错误处理的常见模式是检查 hasError()
并处理错误情况,而不是在没有错误时返回默认错误。errorOr()
更多用于特定的错误处理逻辑,例如错误回退或错误信息替换。
5.3.3 转换操作函数 (Transformation Operation Functions)
folly::Expected
提供了一系列函数式操作,用于转换 Expected
对象的值或错误,或者进行链式操作。这些函数使得可以使用函数式编程风格来处理 Expected
对象,提高代码的可读性和简洁性。
① map(Callable&& transform) &&
和 map(const Callable& transform) &
:
template <typename Callable> auto map(Callable&& transform) && -> Expected<std::invoke_result_t<std::decay_t<Callable>, T>, E>;
template <typename Callable> auto map(const Callable& transform) const& -> Expected<std::invoke_result_t<std::decay_t<Callable>, const T&>, E>;
map()
函数 用于转换 Expected
对象中包含的值。
⚝ 如果 Expected
对象包含值,map()
将该值作为参数传递给提供的 transform
函数(一个可调用对象)。transform
函数的返回值会被包装在一个新的 Expected
对象中返回,作为新的值。错误类型 E
会被传递到新的 Expected
对象。
⚝ 如果 Expected
对象包含错误,map()
不会执行任何操作,直接返回包含相同错误的新 Expected
对象。
map()
函数实现了 Functor 的 map 操作,允许在不影响错误状态的情况下转换成功的值。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
folly::Expected<int, std::string> getIntExpected() {
6
return 10;
7
}
8
9
folly::Expected<int, std::string> getErrorExpected() {
10
return folly::makeUnexpected("Failed to get value");
11
}
12
13
int main() {
14
auto mappedValueResult = getIntExpected().map([](int val) {
15
return val * 2; // 将值乘以 2
16
});
17
if (mappedValueResult.hasValue()) {
18
std::cout << "Mapped value: " << mappedValueResult.value() << std::endl; // 输出: Mapped value: 20
19
}
20
21
auto mappedErrorResult = getErrorExpected().map([](int val) {
22
return val * 2; // 即使 map 被调用,但由于原始 Expected 包含错误,这里不会执行
23
});
24
if (mappedErrorResult.hasError()) {
25
std::cerr << "Mapped error: " << mappedErrorResult.error() << std::endl; // 输出: Mapped error: Failed to get value
26
}
27
return 0;
28
}
② mapError(ErrorCallable&& transformError) &&
和 mapError(const ErrorCallable& transformError) &
:
template <typename ErrorCallable> auto mapError(ErrorCallable&& transformError) && -> Expected<T, std::invoke_result_t<std::decay_t<ErrorCallable>, E>>;
template <typename ErrorCallable> auto mapError(const ErrorCallable& transformError) const& -> Expected<T, std::invoke_result_t<std::decay_t<ErrorCallable>, const E&>>;
mapError()
函数 用于转换 Expected
对象中包含的错误。
⚝ 如果 Expected
对象包含错误,mapError()
将该错误作为参数传递给提供的 transformError
函数(一个可调用对象)。transformError
函数的返回值会被包装在一个新的 Expected
对象中返回,作为新的错误。值类型 T
会被传递到新的 Expected
对象。
⚝ 如果 Expected
对象包含值,mapError()
不会执行任何操作,直接返回包含相同值的新 Expected
对象。
mapError()
允许在不影响成功值的情况下转换错误信息,例如将一种错误类型转换为另一种错误类型,或者为错误信息添加上下文。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
folly::Expected<int, int> getIntErrorExpected() {
6
return folly::makeUnexpected(101); // 错误码 101
7
}
8
9
folly::Expected<int, int> getValueExpectedForErrorMap() {
10
return 50;
11
}
12
13
int main() {
14
auto mappedErrorResult = getIntErrorExpected().mapError([](int errorCode) {
15
return std::string("Error code: ") + std::to_string(errorCode); // 将错误码转换为错误字符串
16
});
17
if (mappedErrorResult.hasError()) {
18
std::cerr << "Mapped error: " << mappedErrorResult.error() << std::endl; // 输出: Mapped error: Error code: 101
19
}
20
21
auto mappedValueErrorResult = getValueExpectedForErrorMap().mapError([](int errorCode) {
22
return std::string("Error code: ") + std::to_string(errorCode); // 即使 mapError 被调用,但由于原始 Expected 包含值,这里不会执行
23
});
24
if (mappedValueErrorResult.hasValue()) {
25
std::cout << "Mapped value (no error map): " << mappedValueErrorResult.value() << std::endl; // 输出: Mapped value (no error map): 50
26
}
27
return 0;
28
}
③ andThen(Callable&& nextOperation) &&
和 andThen(const Callable& nextOperation) &
:
template <typename Callable> auto andThen(Callable&& nextOperation) && -> decltype(invoke(std::forward<Callable>(nextOperation), std::move(this->value())));
template <typename Callable> auto andThen(const Callable& nextOperation) const& -> decltype(invoke(std::forward<Callable>(nextOperation), this->value()));
andThen()
函数(也称为 flatMap
或 bind
)用于 链式操作 Expected
对象,并组合结果。
⚝ 如果 Expected
对象包含值,andThen()
将该值作为参数传递给 nextOperation
函数。nextOperation
函数必须返回另一个 Expected
对象。andThen()
返回 nextOperation
返回的 Expected
对象。
⚝ 如果 Expected
对象包含错误,andThen()
不会执行 nextOperation
,直接返回包含相同错误的新 Expected
对象。
andThen()
是 Monad 的 bind 操作,允许构建一系列依赖于前一步骤成功结果的操作,形成一个处理链。如果链中的任何一步失败(返回包含错误的 Expected
),整个链条会短路,并返回第一个遇到的错误。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
folly::Expected<int, std::string> getPositiveInt(int input) {
6
if (input > 0) {
7
return input;
8
} else {
9
return folly::makeUnexpected("Input is not positive");
10
}
11
}
12
13
folly::Expected<int, std::string> multiplyByTwo(int input) {
14
return input * 2;
15
}
16
17
int main() {
18
auto chainedResult1 = getPositiveInt(5).andThen(multiplyByTwo); // 链式操作,5 -> 10
19
if (chainedResult1.hasValue()) {
20
std::cout << "Chained result 1: " << chainedResult1.value() << std::endl; // 输出: Chained result 1: 10
21
}
22
23
auto chainedResult2 = getPositiveInt(-1).andThen(multiplyByTwo); // getPositiveInt 返回错误,链式操作短路
24
if (chainedResult2.hasError()) {
25
std::cerr << "Chained result 2 error: " << chainedResult2.error() << std::endl; // 输出: Chained result 2 error: Input is not positive
26
}
27
return 0;
28
}
④ orElse(RecoveryCallable&& recovery) &&
和 orElse(const RecoveryCallable& recovery) &
:
template <typename RecoveryCallable> auto orElse(RecoveryCallable&& recovery) && -> decltype(invoke(std::forward<RecoveryCallable>(recovery), std::move(this->error())));
template <typename RecoveryCallable> auto orElse(const RecoveryCallable& recovery) const& -> decltype(invoke(std::forward<RecoveryCallable>(recovery), this->error()));
orElse()
函数用于 处理错误情况,提供错误恢复机制。
⚝ 如果 Expected
对象包含错误,orElse()
将该错误作为参数传递给 recovery
函数。recovery
函数必须返回另一个 Expected
对象。orElse()
返回 recovery
函数返回的 Expected
对象。
⚝ 如果 Expected
对象包含值,orElse()
不会执行 recovery
,直接返回包含相同值的新 Expected
对象。
orElse()
允许在错误发生时尝试恢复,例如提供备用值、重试操作、或者将错误转换为成功结果(在特定场景下)。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
folly::Expected<int, std::string> attemptOperation() {
6
// 模拟可能失败的操作
7
if (rand() % 2 == 0) {
8
return 100; // 成功
9
} else {
10
return folly::makeUnexpected("Operation failed initially"); // 失败
11
}
12
}
13
14
folly::Expected<int, std::string> recoverFromError(const std::string& error) {
15
std::cerr << "Recovering from error: " << error << std::endl;
16
return 50; // 错误恢复,返回备用值 50
17
}
18
19
int main() {
20
auto recoveredResult1 = attemptOperation().orElse(recoverFromError); // 可能成功,可能失败后恢复
21
if (recoveredResult1.hasValue()) {
22
std::cout << "Recovered result 1: " << recoveredResult1.value() << std::endl; // 可能输出 100 或 50
23
}
24
25
auto successfulResult = getPositiveInt(10).orElse(recoverFromError); // getPositiveInt 成功,orElse 不执行
26
if (successfulResult.hasValue()) {
27
std::cout << "Successful result (orElse not called): " << successfulResult.value() << std::endl; // 输出: Successful result (orElse not called): 10
28
}
29
return 0;
30
}
⑤ transform(GeneralCallable&& generalTransform) &&
和 transform(const GeneralCallable& generalTransform) &
:
template <typename GeneralCallable> auto transform(GeneralCallable&& generalTransform) && -> std::invoke_result_t<std::decay_t<GeneralCallable>, Expected<T, E>>;
template <typename GeneralCallable> auto transform(const GeneralCallable& generalTransform) const& -> std::invoke_result_t<std::decay_t<GeneralCallable>, const Expected<T, E>&>;
transform()
函数提供了一种 通用的转换方式,可以同时处理值和错误。
⚝ transform()
接受一个函数 generalTransform
,该函数以整个 Expected
对象作为参数。
⚝ generalTransform
函数的返回值将作为 transform()
函数的返回值。generalTransform
函数需要根据 Expected
对象的状态(包含值或错误)来决定如何转换,并返回适当的结果。
transform()
比 map()
、mapError()
、andThen()
、orElse()
更为通用,因为它允许用户自定义更复杂的转换逻辑,可以同时考虑值和错误状态。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
folly::Expected<int, std::string> processValue(int value) {
6
return value * 3;
7
}
8
9
folly::Expected<int, std::string> handleErr(const std::string& err) {
10
std::cerr << "Handling error in transform: " << err << std::endl;
11
return folly::makeUnexpected(std::string("Handled error: ") + err); // 转换错误
12
}
13
14
int main() {
15
auto transformResult1 = getPositiveInt(7).transform([](folly::Expected<int, std::string> expected) {
16
if (expected.hasValue()) {
17
return processValue(expected.value()); // 处理值
18
} else {
19
return handleErr(expected.error()); // 处理错误
20
}
21
});
22
if (transformResult1.hasValue()) {
23
std::cout << "Transform result 1 value: " << transformResult1.value() << std::endl; // 输出: Transform result 1 value: 21
24
} else {
25
std::cerr << "Transform result 1 error: " << transformResult1.error() << std::endl;
26
}
27
28
auto transformResult2 = getPositiveInt(-5).transform([](folly::Expected<int, std::string> expected) {
29
if (expected.hasValue()) {
30
return processValue(expected.value());
31
} else {
32
return handleErr(expected.error()); // 处理错误
33
}
34
});
35
if (transformResult2.hasValue()) {
36
std::cout << "Transform result 2 value: " << transformResult2.value() << std::endl;
37
} else {
38
std::cerr << "Transform result 2 error: " << transformResult2.error() << std::endl; // 输出: Handling error in transform: Input is not positive, Transform result 2 error: Handled error: Input is not positive
39
}
40
return 0;
41
}
5.3.4 其他实用函数 (Other Utility Functions)
除了状态检查、访问和转换函数外,folly::Expected
还提供了一些其他实用的成员函数,以增强其功能和灵活性。
① swap(Expected& other) noexcept
:
void swap(Expected& other) noexcept;
swap()
函数 交换两个 Expected
对象的内容。这个操作是高效且不抛出异常的。swap()
允许在不进行拷贝或移动构造的情况下,快速交换两个 Expected
对象的状态(包括值或错误)。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
int main() {
5
folly::Expected<int, std::string> expected1 = 100;
6
folly::Expected<int, std::string> expected2 = folly::makeUnexpected("Initial error");
7
8
std::cout << "Before swap:" << std::endl;
9
std::cout << "expected1 has value: " << expected1.hasValue() << std::endl; // true
10
std::cout << "expected2 has error: " << expected2.hasError() << std::endl; // true
11
12
expected1.swap(expected2); // 交换内容
13
14
std::cout << "\nAfter swap:" << std::endl;
15
std::cout << "expected1 has error: " << expected1.hasError() << std::endl; // true (现在包含之前的 expected2 的错误)
16
std::cout << "expected2 has value: " << expected2.hasValue() << std::endl; // true (现在包含之前的 expected1 的值)
17
18
if (expected1.hasError()) std::cerr << "expected1 error: " << expected1.error() << std::endl; // 输出: expected1 error: Initial error
19
if (expected2.hasValue()) std::cout << "expected2 value: " << expected2.value() << std::endl; // 输出: expected2 value: 100
20
21
return 0;
22
}
② emplace(Args&&... args)
:
template <typename... Args> T& emplace(Args&&... args);
emplace()
函数 在 Expected
对象内部直接构造值对象。
⚝ 如果 Expected
对象当前包含错误或未初始化,emplace()
会销毁已有的内容(如果有),然后在原地构造一个新的值对象,使用提供的参数 args...
转发给值类型 T
的构造函数。
⚝ emplace()
返回对新构造的值对象的引用 T&
。
⚝ 如果 Expected
对象已经包含值,调用 emplace()
可能会导致未定义行为。通常,emplace()
用于在确定要存储值时,高效地构造值对象,避免额外的拷贝或移动操作。
1
#include <folly/Expected.h>
2
#include <string>
3
#include <iostream>
4
5
struct MyValue {
6
MyValue(int a, const std::string& b) : val1(a), val2(b) {
7
std::cout << "MyValue constructed with: " << val1 << ", " << val2 << std::endl;
8
}
9
int val1;
10
std::string val2;
11
};
12
13
int main() {
14
folly::Expected<MyValue, std::string> expectedValue; // 默认构造,未初始化
15
16
expectedValue.emplace(42, "Hello"); // 原地构造 MyValue 对象
17
if (expectedValue.hasValue()) {
18
std::cout << "Emplaced value: " << expectedValue.value().val1 << ", " << expectedValue.value().val2 << std::endl;
19
// 输出: MyValue constructed with: 42, Hello
20
// 输出: Emplaced value: 42, Hello
21
}
22
23
return 0;
24
}
注意: emplace()
主要用于优化性能,特别是在值类型 T
的构造开销较大时。使用 emplace()
可以避免临时对象的创建和拷贝。
5.4 非成员函数 (Non-member Functions)
除了成员函数,folly::Expected
也提供了一些非成员函数,用于扩展其功能,例如比较操作和交换操作。
5.4.1 比较运算符 (Comparison Operators)
folly::Expected
重载了比较运算符,允许直接比较两个 Expected
对象。比较操作符包括:
① operator==(const Expected<T, E>& lhs, const Expected<T, E>& rhs)
:
相等运算符 ==
比较两个 Expected
对象是否相等。
⚝ 两个 Expected
对象相等,当且仅当它们都包含值,且值相等(使用 T
类型的 operator==
),或者它们都包含错误,且错误相等(使用 E
类型的 operator==
)。
⚝ 如果一个 Expected
包含值,另一个包含错误,则它们不相等。
② operator!=(const Expected<T, E>& lhs, const Expected<T, E>& rhs)
:
不等运算符 !=
比较两个 Expected
对象是否不相等。其结果是 !(lhs == rhs)
。
③ operator<(const Expected<T, E>& lhs, const Expected<T, E>& rhs)
, operator>(const Expected<T, E>& lhs, const Expected<T, E>& rhs)
, operator<=(const Expected<T, E>& lhs, const Expected<T, E>& rhs)
, operator>=(const Expected<T, E>& lhs, const Expected<T, E>& rhs)
:
关系运算符 <
、>
、<=
、>=
也被重载,用于比较两个 Expected
对象。
⚝ 比较操作首先比较 Expected
对象的状态:包含值被认为“小于”包含错误。
⚝ 如果两个 Expected
对象都包含值,则比较它们的值(使用 T
类型的相应运算符)。
⚝ 如果两个 Expected
对象都包含错误,则比较它们的错误(使用 E
类型的相应运算符)。
注意: 关系运算符的重载需要值类型 T
和错误类型 E
分别支持相应的比较运算符。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
int main() {
5
folly::Expected<int, std::string> expected1 = 10;
6
folly::Expected<int, std::string> expected2 = 10;
7
folly::Expected<int, std::string> expected3 = 20;
8
folly::Expected<int, std::string> expectedError1 = folly::makeUnexpected("Error 1");
9
folly::Expected<int, std::string> expectedError2 = folly::makeUnexpected("Error 1");
10
folly::Expected<int, std::string> expectedError3 = folly::makeUnexpected("Error 2");
11
12
std::cout << "expected1 == expected2: " << (expected1 == expected2) << std::endl; // true
13
std::cout << "expected1 == expected3: " << (expected1 == expected3) << std::endl; // false
14
std::cout << "expected1 == expectedError1: " << (expected1 == expectedError1) << std::endl; // false
15
std::cout << "expectedError1 == expectedError2: " << (expectedError1 == expectedError2) << std::endl; // true
16
std::cout << "expectedError1 == expectedError3: " << (expectedError1 == expectedError3) << std::endl; // false
17
18
std::cout << "expected1 < expected3: " << (expected1 < expected3) << std::endl; // true
19
std::cout << "expected3 < expected1: " << (expected3 < expected1) << std::endl; // false
20
std::cout << "expected1 < expectedError1: " << (expected1 < expectedError1) << std::endl; // true (值 < 错误)
21
std::cout << "expectedError1 < expected1: " << (expectedError1 < expected1) << std::endl; // false (错误 > 值)
22
std::cout << "expectedError1 < expectedError3: " << (expectedError1 < expectedError3) << std::endl; // true ("Error 1" < "Error 2" 字符串比较)
23
24
return 0;
25
}
5.4.2 交换函数 (Swap Function)
template <typename T, typename E> void swap(Expected<T, E>& a, Expected<T, E>& b) noexcept
这是一个非成员函数 swap
,提供了交换两个 Expected
对象内容的全局方法。它与成员函数 swap()
功能相同,但可以用于在不通过对象实例调用的情况下交换两个 Expected
对象。
1
#include <folly/Expected.h>
2
#include <iostream>
3
4
int main() {
5
folly::Expected<int, std::string> expected1 = 555;
6
folly::Expected<int, std::string> expected2 = folly::makeUnexpected("Initial state");
7
8
std::cout << "Before swap (non-member):" << std::endl;
9
std::cout << "expected1 has value: " << expected1.hasValue() << std::endl; // true
10
std::cout << "expected2 has error: " << expected2.hasError() << std::endl; // true
11
12
swap(expected1, expected2); // 使用非成员 swap 函数
13
14
std::cout << "\nAfter swap (non-member):" << std::endl;
15
std::cout << "expected1 has error: " << expected1.hasError() << std::endl; // true (现在包含之前的 expected2 的错误)
16
std::cout << "expected2 has value: " << expected2.hasValue() << std::endl; // true (现在包含之前的 expected1 的值)
17
18
if (expected1.hasError()) std::cerr << "expected1 error: " << expected1.error() << std::endl; // 输出: expected1 error: Initial state
19
if (expected2.hasValue()) std::cout << "expected2 value: " << expected2.value() << std::endl; // 输出: expected2 value: 555
20
21
return 0;
22
}
非成员 swap
函数通常在泛型编程中很有用,因为它允许在不知道对象具体类型的情况下进行交换操作,只要类型支持 swap
操作即可。对于 folly::Expected
,成员和非成员 swap
函数都提供了高效且安全的交换机制。
END_OF_CHAPTER
6. chapter 6:Expected 的最佳实践与陷阱 (Best Practices and Pitfalls of Expected)
6.1 何时使用 Expected (When to Use Expected)
6.1.1 函数可能失败但非致命错误 (Functions May Fail but Non-Fatal Errors)
在软件开发中,函数可能会因为各种原因执行失败。这些失败有些是致命的,意味着程序无法继续运行,例如内存耗尽或硬件故障。而另一些失败则是非致命的,表示操作未能成功完成,但程序仍然可以尝试恢复或继续执行其他任务。Expected
最适合处理非致命错误的场景。
① 非致命错误的定义:非致命错误通常指的是那些可以被预见和处理的错误,例如:
▮▮▮▮ⓑ 文件未找到:当程序尝试打开一个不存在的文件时。
▮▮▮▮ⓒ 网络请求超时:当程序向远程服务器发送请求,但服务器在规定时间内没有响应。
▮▮▮▮ⓓ 输入数据格式错误:当用户输入的数据不符合程序要求的格式,例如期望整数却输入了字符串。
▮▮▮▮ⓔ 资源暂时不可用:例如,数据库连接池暂时耗尽,或者文件系统空间不足。
② Expected
的适用性:对于这类非致命错误,使用 Expected
可以清晰地表达函数可能返回一个值(操作成功)或者一个错误(操作失败)。它迫使调用者显式地处理错误情况,而不是像异常那样可能被忽略,或者像错误码那样容易被遗忘检查。
③ 代码示例:考虑一个文件读取函数。如果文件存在且可读,函数应该返回文件内容;如果文件不存在或无法读取,则应该返回一个错误信息。使用 Expected
可以优雅地表达这种逻辑。
1
#include <fstream>
2
#include <string>
3
#include <system_error>
4
#include <folly/Expected.h>
5
6
using namespace folly;
7
8
enum class FileError {
9
NotFound,
10
PermissionDenied,
11
ReadError
12
};
13
14
Expected<std::string, FileError> readFile(const std::string& filename) {
15
std::ifstream file(filename);
16
if (!file.is_open()) {
17
if (!std::filesystem::exists(filename)) {
18
return makeUnexpected(FileError::NotFound);
19
} else {
20
return makeUnexpected(FileError::PermissionDenied);
21
}
22
}
23
24
std::string content;
25
std::string line;
26
while (std::getline(file, line)) {
27
content += line + "\n";
28
}
29
if (file.fail() && !file.eof()) { // 检查读取错误,排除 eof
30
return makeUnexpected(FileError::ReadError);
31
}
32
return content;
33
}
34
35
void processFile(const std::string& filename) {
36
auto contentOrError = readFile(filename);
37
if (contentOrError.hasValue()) {
38
std::cout << "File content:\n" << contentOrError.value() << std::endl;
39
} else {
40
switch (contentOrError.error()) {
41
case FileError::NotFound:
42
std::cerr << "Error: File not found: " << filename << std::endl;
43
break;
44
case FileError::PermissionDenied:
45
std::cerr << "Error: Permission denied for file: " << filename << std::endl;
46
break;
47
case FileError::ReadError:
48
std::cerr << "Error: Failed to read file: " << filename << std::endl;
49
break;
50
default:
51
std::cerr << "Error: Unknown file error." << std::endl;
52
break;
53
}
54
}
55
}
56
57
int main() {
58
processFile("example.txt"); // 假设 example.txt 不存在
59
processFile("valid_file.txt"); // 假设 valid_file.txt 存在且可读
60
return 0;
61
}
在这个例子中,readFile
函数使用 Expected<std::string, FileError>
来返回结果。如果文件读取成功,它返回 Expected
包含文件内容;如果失败,则返回包含 FileError
枚举值的 Unexpected
。processFile
函数则展示了如何检查 Expected
的状态并处理成功或失败的情况。
6.1.2 需要清晰区分正常结果与错误 (Need to Clearly Distinguish Normal Results from Errors)
在某些情况下,函数的返回值可能既表示正常结果,也可能表示错误状态,这会导致代码的语义模糊不清,增加理解和维护的难度。Expected
在这种场景下特别有用,因为它强制函数返回类型明确区分值和错误,从而提高代码的清晰度和可读性。
① 语义模糊的返回值:考虑一个返回整数的函数,例如,一个查找列表中元素索引的函数。如果元素存在,函数返回索引(非负整数);如果元素不存在,函数可能返回 -1
或者其他特殊值来表示错误。这种做法存在以下问题:
▮▮▮▮ⓑ 魔术数字:使用 -1
这样的魔术数字来表示错误,含义不直观,容易被误解。
▮▮▮▮ⓒ 类型混淆:返回值类型都是整数,无法在类型层面区分正常结果和错误状态,需要额外的文档或约定来解释返回值含义。
▮▮▮▮ⓓ 错误处理容易被忽略:调用者可能会忘记检查返回值是否为 -1
,导致错误被忽略。
② Expected
的优势:Expected
通过类型系统强制区分正常结果和错误。函数返回 Expected<T, E>
,明确表示可能返回类型为 T
的值(成功)或类型为 E
的错误(失败)。这使得代码的意图更加清晰,并减少了错误处理被忽略的可能性。
③ 代码示例:假设我们需要一个函数来解析字符串为整数。如果字符串是有效的整数,函数返回解析后的整数;如果字符串不是有效的整数,则返回一个错误。
1
#include <string>
2
#include <string_view>
3
#include <system_error>
4
#include <folly/Expected.h>
5
#include <stdexcept> // for std::invalid_argument
6
7
using namespace folly;
8
9
enum class ParseError {
10
InvalidFormat,
11
Overflow
12
};
13
14
Expected<int, ParseError> parseInt(std::string_view str) {
15
try {
16
size_t pos = 0;
17
int result = std::stoi(std::string(str), &pos);
18
if (pos != str.length()) { // 检查是否整个字符串都被转换了
19
return makeUnexpected(ParseError::InvalidFormat);
20
}
21
return result;
22
} catch (const std::invalid_argument&) {
23
return makeUnexpected(ParseError::InvalidFormat);
24
} catch (const std::out_of_range&) {
25
return makeUnexpected(ParseError::Overflow);
26
}
27
}
28
29
void processInput(std::string_view input) {
30
auto intOrError = parseInt(input);
31
if (intOrError.hasValue()) {
32
std::cout << "Parsed integer: " << intOrError.value() << std::endl;
33
} else {
34
switch (intOrError.error()) {
35
case ParseError::InvalidFormat:
36
std::cerr << "Error: Invalid integer format: " << input << std::endl;
37
break;
38
case ParseError::Overflow:
39
std::cerr << "Error: Integer overflow: " << input << std::endl;
40
break;
41
default:
42
std::cerr << "Error: Unknown parsing error." << std::endl;
43
break;
44
}
45
}
46
}
47
48
int main() {
49
processInput("123");
50
processInput("abc");
51
processInput("999999999999999999999999999999"); // 假设超出 int 范围
52
processInput("123xyz"); // 部分有效数字
53
return 0;
54
}
在这个例子中,parseInt
函数使用 Expected<int, ParseError>
来返回结果。如果解析成功,返回 Expected
包含解析后的整数;如果解析失败(例如,无效格式或溢出),则返回包含 ParseError
枚举值的 Unexpected
。processInput
函数清晰地处理了成功和失败两种情况,提高了代码的可读性和健壮性。
6.2 避免过度使用 Expected (Avoiding Overuse of Expected)
虽然 Expected
在错误处理方面有很多优点,但并不意味着所有函数都应该返回 Expected
。过度使用 Expected
可能会导致代码变得冗余和复杂,反而降低了代码的可读性和性能。我们需要根据具体的场景权衡利弊,合理使用 Expected
。
6.2.1 简单错误处理场景 (Simple Error Handling Scenarios)
对于一些简单的错误处理场景,使用 Expected
可能显得过于繁琐。例如,如果一个函数不太可能失败,或者即使失败了,错误处理逻辑也非常简单,那么使用传统的错误处理方式可能更加简洁高效。
① 不太可能失败的函数:有些函数的设计目标就是尽可能避免失败,或者失败的概率极低。例如,一些纯计算函数,只要输入参数有效,通常都能成功执行。对于这类函数,如果为了极低的失败概率而引入 Expected
,可能会增加不必要的复杂性。
② 简单错误处理逻辑:如果函数的错误处理逻辑非常简单,例如,仅仅是记录一个日志或者返回一个默认值,那么使用 Expected
可能显得过于重量级。在这种情况下,使用 std::optional
或者简单的条件判断可能更加合适。
③ 代码示例:考虑一个简单的加法函数。假设我们认为加法操作不太可能失败(除非发生非常严重的系统错误,但这通常不是我们用 Expected
处理的范围)。
1
#include <iostream>
2
#include <folly/Expected.h>
3
#include <stdexcept>
4
5
using namespace folly;
6
7
// 简单加法函数,不太可能失败
8
int add(int a, int b) {
9
return a + b;
10
}
11
12
// 使用 std::optional 处理可能为空的情况 (与错误处理无关,但作为对比)
13
std::optional<int> divide(int a, int b) {
14
if (b == 0) {
15
return std::nullopt; // 使用 std::optional 表示除数为零的情况
16
}
17
return a / b;
18
}
19
20
21
int main() {
22
int sum = add(5, 3);
23
std::cout << "Sum: " << sum << std::endl;
24
25
auto result = divide(10, 2);
26
if (result.has_value()) {
27
std::cout << "Division result: " << result.value() << std::endl;
28
} else {
29
std::cerr << "Error: Division by zero." << std::endl;
30
}
31
32
return 0;
33
}
在这个例子中,add
函数直接返回 int
,因为它被设计为不太可能失败。对于可能出现 "无结果" 情况的 divide
函数,我们使用了 std::optional
来表示可能没有有效值的情况,而不是错误。在这些简单场景下,过度使用 Expected
反而会使代码显得笨重。
6.2.2 性能敏感型代码 (Performance-Sensitive Code)
在性能至关重要的代码路径中,错误处理的开销也需要仔细考虑。虽然 Expected
的设计目标是低开销,但在某些极端性能敏感的场景下,额外的状态检查和可能的错误对象构造仍然可能成为性能瓶颈。因此,在性能敏感型代码中,需要权衡 Expected
的错误处理优势和潜在的性能开销。
① 性能开销来源:Expected
的性能开销主要来自于:
▮▮▮▮ⓑ 状态检查:每次使用 Expected
对象时,都需要检查其状态 (hasValue()
或 hasError()
),这会引入额外的条件判断。
▮▮▮▮ⓒ 错误对象构造:当函数返回错误时,需要构造错误对象,这可能涉及内存分配和拷贝操作,尤其当错误类型比较复杂时。
▮▮▮▮ⓓ 间接访问:访问 Expected
中的值或错误需要通过 value()
或 error()
函数,这可能比直接访问返回值略微增加间接访问的开销。
② 性能优化策略:在性能敏感型代码中,可以考虑以下优化策略:
▮▮▮▮ⓑ 减少状态检查:如果能确保在某些代码路径中 Expected
必然包含值或错误,可以避免不必要的状态检查。例如,在 map
或 andThen
链式调用中,可以利用其短路求值特性,减少显式状态检查。
▮▮▮▮ⓒ 选择轻量级错误类型:使用简单的枚举或轻量级结构体作为错误类型,避免复杂的错误对象构造和拷贝开销。
▮▮▮▮ⓓ 内联和编译优化:现代 C++ 编译器通常能够很好地优化 Expected
的代码,例如内联函数调用和消除不必要的拷贝。可以通过编译优化选项 (-O2
, -O3
) 来提升性能。
▮▮▮▮ⓔ 性能测试和分析:在关键性能路径上,进行基准测试和性能分析,评估 Expected
的实际性能影响,并根据测试结果进行调整。
③ 代码示例:假设有一个性能敏感的数值计算函数,需要处理可能出现的除零错误。
1
#include <iostream>
2
#include <folly/Expected.h>
3
#include <chrono>
4
5
using namespace folly;
6
using namespace std::chrono;
7
8
enum class MathError {
9
DivideByZero
10
};
11
12
// 使用 Expected 的除法函数
13
Expected<double, MathError> safeDivideExpected(double a, double b) {
14
if (b == 0.0) {
15
return makeUnexpected(MathError::DivideByZero);
16
}
17
return a / b;
18
}
19
20
// 传统异常处理的除法函数
21
double safeDivideException(double a, double b) {
22
if (b == 0.0) {
23
throw std::runtime_error("Divide by zero");
24
}
25
return a / b;
26
}
27
28
int main() {
29
int iterations = 1000000;
30
31
// Expected 版本性能测试
32
auto startExpected = high_resolution_clock::now();
33
for (int i = 0; i < iterations; ++i) {
34
auto result = safeDivideExpected(10.0, i == 0 ? 1.0 : 2.0); // 避免每次都除零,模拟正常和错误情况
35
if (result.hasError() && result.error() == MathError::DivideByZero) {
36
// 处理错误 (这里为了性能测试,只做简单判断)
37
}
38
}
39
auto endExpected = high_resolution_clock::now();
40
auto durationExpected = duration_cast<milliseconds>(endExpected - startExpected);
41
42
// 异常版本性能测试
43
auto startException = high_resolution_clock::now();
44
for (int i = 0; i < iterations; ++i) {
45
try {
46
double result = safeDivideException(10.0, i == 0 ? 1.0 : 2.0);
47
} catch (const std::runtime_error& e) {
48
// 处理异常 (这里为了性能测试,只做简单捕获)
49
}
50
}
51
auto endException = high_resolution_clock::now();
52
auto durationException = duration_cast<milliseconds>(endException - startException);
53
54
std::cout << "Expected version duration: " << durationExpected.count() << " milliseconds" << std::endl;
55
std::cout << "Exception version duration: " << durationException.count() << " milliseconds" << std::endl;
56
57
return 0;
58
}
这个简单的性能测试比较了 Expected
和异常在循环中的性能。实际性能差异会受到编译器优化、错误发生频率、错误类型复杂度等多种因素影响。在性能敏感型代码中,建议进行实际测试,并根据测试结果选择合适的错误处理方式。通常情况下,Expected
的性能开销是可接受的,但极端情况下可能需要仔细评估。
6.3 常见的陷阱与错误用法 (Common Pitfalls and Misuses)
即使理解了 Expected
的基本概念和最佳实践,开发者仍然可能在使用过程中犯一些常见的错误。了解这些陷阱可以帮助我们避免错误用法,更有效地利用 Expected
。
6.3.1 忽略错误 (Ignoring Errors)
最常见的错误用法之一就是忽略 Expected
返回的错误。Expected
的设计初衷是强制开发者显式地处理错误情况,但如果开发者不检查 Expected
的状态,直接使用 value()
访问值,当 Expected
包含错误时,程序会抛出异常,导致程序崩溃或行为异常。
① 错误示例:
1
#include <iostream>
2
#include <folly/Expected.h>
3
4
using namespace folly;
5
6
Expected<int, std::string> mayFail() {
7
return makeUnexpected("Something went wrong");
8
}
9
10
int main() {
11
auto result = mayFail();
12
int value = result.value(); // 错误:如果 result 包含错误,这里会抛出异常
13
std::cout << "Value: " << value << std::endl;
14
return 0;
15
}
在这个例子中,mayFail
函数返回一个包含错误的 Expected
,但 main
函数直接调用 value()
访问值,而没有检查 result.hasValue()
。这会导致程序抛出 std::bad_expected_access<std::string>
异常。
② 正确做法:始终检查 Expected
的状态,再访问值或错误。
1
#include <iostream>
2
#include <folly/Expected.h>
3
4
using namespace folly;
5
6
Expected<int, std::string> mayFail() {
7
return makeUnexpected("Something went wrong");
8
}
9
10
int main() {
11
auto result = mayFail();
12
if (result.hasValue()) {
13
int value = result.value();
14
std::cout << "Value: " << value << std::endl;
15
} else {
16
std::cerr << "Error: " << result.error() << std::endl;
17
}
18
return 0;
19
}
这段代码首先检查 result.hasValue()
,只有当 Expected
包含值时才调用 value()
,否则处理错误情况,避免了程序崩溃。
③ 使用函数式操作:利用 Expected
提供的函数式操作(如 map
, andThen
, orElse
)可以更安全地处理 Expected
,减少手动状态检查的需求。这些函数式操作会自动处理 Expected
的状态,并根据状态执行相应的操作。
1
#include <iostream>
2
#include <folly/Expected.h>
3
4
using namespace folly;
5
6
Expected<int, std::string> mayFail() {
7
return makeUnexpected("Something went wrong");
8
}
9
10
int main() {
11
auto result = mayFail()
12
.map([](int value) { return value * 2; }) // 只有在成功时才执行
13
.orElse([](const std::string& error) {
14
std::cerr << "Error: " << error << std::endl;
15
return Expected<int, std::string>(0); // 错误恢复,返回默认值
16
});
17
18
if (result.hasValue()) {
19
std::cout << "Result: " << result.value() << std::endl;
20
}
21
return 0;
22
}
在这个例子中,map
和 orElse
函数式操作链式调用,自动处理了 Expected
的状态。map
只在 Expected
包含值时执行,orElse
在 Expected
包含错误时执行,并提供了错误处理和恢复机制。
6.3.2 不恰当的错误类型 (Inappropriate Error Types)
选择合适的错误类型对于 Expected
的有效使用至关重要。不恰当的错误类型会降低错误信息的表达能力,增加错误处理的难度。
① 使用 void
或 std::error_code
作为错误类型:虽然 Expected<T, void>
或 Expected<T, std::error_code>
在某些简单场景下可以使用,但它们通常不足以提供丰富的错误信息。void
类型无法传递任何错误信息,std::error_code
虽然可以表示一些标准错误,但自定义错误场景下表达能力有限。
② 错误示例:
1
#include <iostream>
2
#include <folly/Expected.h>
3
4
using namespace folly;
5
6
Expected<int, void> operation() {
7
// ... 某些操作可能失败 ...
8
return makeUnexpected(); // 使用 void 作为错误类型,无法传递错误信息
9
}
10
11
int main() {
12
auto result = operation();
13
if (result.hasValue()) {
14
std::cout << "Result: " << result.value() << std::endl;
15
} else {
16
std::cerr << "Error: Operation failed." << std::endl; // 只能输出笼统的错误信息
17
}
18
return 0;
19
}
在这个例子中,operation
函数使用 void
作为错误类型,当操作失败时,只能返回一个 Unexpected<void>
,调用者只能知道操作失败了,但无法获取更详细的错误信息。
③ 推荐做法:使用自定义的枚举类或类作为错误类型,以便提供更具体、更丰富的错误信息。如 6.1.1 和 6.1.2 节的例子所示,使用 enum class FileError
和 enum class ParseError
可以清晰地表达不同类型的错误,方便调用者根据错误类型进行精细化的错误处理。
④ 错误类型的设计原则:
▮▮▮▮ⓑ 信息丰富:错误类型应该能够提供足够的信息,帮助调用者理解错误原因和上下文。
▮▮▮▮ⓒ 类型安全:使用枚举类或类作为错误类型,可以提高类型安全性,避免使用魔术数字或字符串表示错误。
▮▮▮▮ⓓ 可扩展性:错误类型设计应该具有一定的可扩展性,方便在未来添加新的错误类型或错误信息。
▮▮▮▮ⓔ 易于处理:错误类型应该易于被调用者处理,例如,可以使用 switch
语句或模式匹配来处理不同类型的错误。
6.3.3 过度嵌套的 Expected (Excessively Nested Expected)
在复杂的业务逻辑中,可能会出现函数调用链很长,每个函数都返回 Expected
的情况,导致代码中出现过度嵌套的 Expected
,降低代码的可读性和可维护性。
① 嵌套示例:
1
#include <iostream>
2
#include <folly/Expected.h>
3
4
using namespace folly;
5
6
Expected<int, std::string> funcA() { return 10; }
7
Expected<Expected<int, std::string>, std::string> funcB(int value) {
8
if (value > 0) {
9
return funcA(); // 返回 Expected<int, std::string> 嵌套在 Expected 中
10
} else {
11
return makeUnexpected("Invalid value in funcB");
12
}
13
}
14
15
int main() {
16
auto resultB = funcB(5);
17
if (resultB.hasValue()) {
18
auto resultA = resultB.value(); // resultA 的类型是 Expected<int, std::string>,需要再次检查
19
if (resultA.hasValue()) {
20
std::cout << "Value: " << resultA.value() << std::endl;
21
} else {
22
std::cerr << "Error in funcA: " << resultA.error() << std::endl;
23
}
24
} else {
25
std::cerr << "Error in funcB: " << resultB.error() << std::endl;
26
}
27
return 0;
28
}
在这个例子中,funcB
返回 Expected<Expected<int, std::string>, std::string>
,导致 Expected
嵌套了一层。在 main
函数中,需要两层 if (hasValue())
检查才能访问最终的值,代码显得冗余和难以理解。
② 避免嵌套的方法:
▮▮▮▮ⓑ 使用 andThen
(flatMap):andThen
函数可以将返回 Expected
的函数链式调用起来,并自动展平嵌套的 Expected
。
1
#include <iostream>
2
#include <folly/Expected.h>
3
4
using namespace folly;
5
6
Expected<int, std::string> funcA() { return 10; }
7
Expected<int, std::string> funcB(int value) { // 修改 funcB 的返回类型,避免嵌套
8
if (value > 0) {
9
return funcA();
10
} else {
11
return makeUnexpected("Invalid value in funcB");
12
}
13
}
14
15
int main() {
16
auto result = funcB(5)
17
.andThen([](int value) { return funcA(); }); // 使用 andThen 展平嵌套
18
19
if (result.hasValue()) {
20
std::cout << "Value: " << result.value() << std::endl;
21
} else {
22
std::cerr << "Error: " << result.error() << std::endl;
23
}
24
return 0;
25
}
在这个修改后的例子中,funcB
的返回类型被修改为 Expected<int, std::string>
,main
函数中使用 andThen
将 funcB
和 funcA
链式调用起来,避免了 Expected
的嵌套。andThen
的 lambda 函数的返回值会自动被展平,最终 result
的类型仍然是 Expected<int, std::string>
。
▮▮▮▮ⓑ 重新设计函数接口:如果嵌套 Expected
的出现是由于函数接口设计不合理导致的,可以考虑重新设计函数接口,例如,将多个可能失败的操作合并为一个操作,或者将错误处理逻辑上移到调用者。
6.4 与其他 Folly 库的集成 (Integration with Other Folly Libraries)
folly/Expected.h
可以与其他 Folly 库组件很好地集成,共同构建更强大、更灵活的 C++ 应用。其中,与 folly/futures
的集成尤为重要,可以方便地处理异步操作的错误。
6.4.1 与 Futures 的结合使用 (Using with Futures)
folly/futures
提供了异步编程的强大工具,而 Expected
可以很好地处理异步操作可能产生的错误。将 Future
和 Expected
结合使用,可以构建既异步又健壮的程序。
① Future<Expected<T, E>>
类型:当异步操作可能返回一个值或一个错误时,可以使用 Future<Expected<T, E>>
类型来表示异步操作的结果。Future
表示操作的异步性,Expected
表示操作可能成功或失败。
② 异步操作返回 Expected
:异步函数可以返回 Future<Expected<T, E>>
,表示异步操作的结果是一个 Expected
对象。
1
#include <iostream>
2
#include <folly/Expected.h>
3
#include <folly/Future.h>
4
#include <folly/executors/InlineExecutor.h>
5
6
using namespace folly;
7
8
enum class AsyncError {
9
Timeout,
10
NetworkError
11
};
12
13
Future<Expected<int, AsyncError>> asyncOperation() {
14
// 模拟异步操作,例如网络请求
15
return makeFuture(Expected<int, AsyncError>(10))
16
.delayed(milliseconds(100)); // 延迟 100 毫秒
17
}
18
19
void processAsyncResult(Future<Expected<int, AsyncError>> future) {
20
future.then([](Expected<int, AsyncError> result) {
21
if (result.hasValue()) {
22
std::cout << "Async operation result: " << result.value() << std::endl;
23
} else {
24
switch (result.error()) {
25
case AsyncError::Timeout:
26
std::cerr << "Async operation timeout." << std::endl;
27
break;
28
case AsyncError::NetworkError:
29
std::cerr << "Async network error." << std::endl;
30
break;
31
default:
32
std::cerr << "Unknown async error." << std::endl;
33
break;
34
}
35
}
36
}).getVia(&InlineExecutor::instance()); // 使用 InlineExecutor 同步执行 then 回调,实际应用中可以使用其他 Executor
37
}
38
39
int main() {
40
processAsyncResult(asyncOperation());
41
return 0;
42
}
在这个例子中,asyncOperation
函数返回 Future<Expected<int, AsyncError>>
,表示一个异步操作,其结果是一个 Expected
对象。processAsyncResult
函数展示了如何处理异步操作的结果,包括检查 Expected
的状态和处理成功或失败的情况。
③ Future
的函数式操作与 Expected
的结合:folly/futures
提供了丰富的函数式操作(如 then
, map
, flatMap
, catch
),可以与 Expected
的函数式操作(如 map
, andThen
, orElse
)结合使用,构建复杂的异步错误处理流程。
例如,可以使用 Future::thenValue
和 Expected::map
组合,在异步操作成功后对结果值进行转换:
1
Future<Expected<int, AsyncError>> asyncOperation() {
2
return makeFuture(Expected<int, AsyncError>(10)).delayed(milliseconds(100));
3
}
4
5
Future<Expected<int, AsyncError>> processAsyncResult() {
6
return asyncOperation()
7
.thenValue([](Expected<int, AsyncError> result) {
8
return result.map([](int value) { return value * 2; }); // 异步操作成功后,对值进行 map 操作
9
});
10
}
可以使用 Future::then
和 Expected::andThen
组合,在异步操作成功后执行另一个可能返回 Expected
的操作,并展平结果:
1
Future<Expected<int, AsyncError>> asyncOperation1() {
2
return makeFuture(Expected<int, AsyncError>(10)).delayed(milliseconds(100));
3
}
4
5
Future<Expected<int, AsyncError>> asyncOperation2(int value) {
6
return makeFuture(Expected<int, AsyncError>(value * 2)).delayed(milliseconds(100));
7
}
8
9
Future<Expected<int, AsyncError>> processAsyncOperations() {
10
return asyncOperation1()
11
.then([](Try<Expected<int, AsyncError>> tryResult) { // 使用 then 处理 Future<Expected> 的结果
12
if (tryResult.hasException()) {
13
// Future 自身失败的处理 (例如,Promise 被 setException)
14
return makeFuture(makeUnexpected(AsyncError::NetworkError)); // 假设 Future 失败都归为网络错误
15
}
16
Expected<int, AsyncError> expectedResult = tryResult.value();
17
if (expectedResult.hasError()) {
18
return makeFuture(expectedResult); // 传递 Expected 中的错误
19
}
20
return asyncOperation2(expectedResult.value()); // 异步操作1成功,执行异步操作2
21
});
22
}
6.4.2 与其他 Folly 组件的协同工作 (Working with Other Folly Components)
folly/Expected.h
不仅可以与 folly/futures
集成,还可以与其他 Folly 组件协同工作,例如:
① folly/FBString
: FBString
是 Folly 提供的优化字符串类,可以作为 Expected
的值类型或错误类型,尤其在处理大量字符串或性能敏感的场景下,可以提升性能。
② folly/dynamic
: dynamic
是 Folly 提供的动态类型,可以用于表示不确定类型的返回值或错误信息,与 Expected
结合使用,可以处理更灵活的错误场景,例如,API 接口返回的错误信息格式不固定。
③ folly/json
: folly/json
提供了 JSON 解析和生成的功能,可以将 Expected
的值或错误信息序列化为 JSON 格式,方便在网络传输或日志记录中使用。
④ folly/logging
: folly/logging
提供了强大的日志功能,可以将 Expected
的错误信息记录到日志中,方便错误追踪和调试。
通过与 Folly 库的其他组件协同工作,folly/Expected.h
可以更好地融入 Folly 生态系统,发挥更大的作用,帮助开发者构建更健壮、更高效的 C++ 应用。
END_OF_CHAPTER
7. chapter 7:总结与展望 (Summary and Outlook)
7.1 Expected 的优势与局限性总结 (Summary of Advantages and Limitations of Expected)
Expected
,作为 Folly 库中一个强大的工具,为 C++ 错误处理提供了一种现代且类型安全的方法。它在众多场景下展现出显著的优势,但也存在一些局限性。理解这些优势与局限性,能够帮助开发者更好地判断何时以及如何有效地使用 Expected
。
优势 (Advantages):
① 清晰的结果表达 (Clear Result Expression):Expected
最核心的优势在于其能够清晰地表达函数的返回结果,明确指出函数可能返回一个值或一个错误。这种显式的类型信息使得代码的意图更加明确,降低了理解和维护的难度。与传统的错误码或异常相比,Expected
在类型层面就区分了正常结果和错误,避免了歧义和潜在的错误处理遗漏。
② 类型安全 (Type Safety):Expected
是一个模板类,它对值类型 T
和错误类型 E
都有严格的类型约束。这意味着编译器可以在编译时检查类型错误,减少运行时错误的可能性。这种类型安全特性在大型项目中尤为重要,可以显著提高代码的健壮性和可靠性。
③ 函数式编程友好 (Functional Programming Friendly):Expected
提供了 map
、mapError
、andThen
、orElse
和 transform
等函数式操作,使得可以以链式调用的方式处理结果,进行错误传播和转换。这种函数式编程风格可以使代码更加简洁、优雅,并易于组合和测试。
④ 性能可预测 (Predictable Performance):与异常相比,Expected
的错误处理机制通常具有更可预测的性能。异常的抛出和捕获可能涉及栈展开等开销较大的操作,尤其是在频繁发生错误的情况下。而 Expected
的错误处理主要通过值的传递和状态检查来实现,避免了异常处理的开销,在某些性能敏感的场景下,Expected
可能是一个更优的选择。当然,Expected
本身也存在一定的性能开销,例如额外的状态检查和可能的拷贝操作,但这通常比异常处理的开销更可控。
⑤ 与现有代码的兼容性 (Compatibility with Existing Code):Expected
可以与现有的 C++ 代码良好地集成。它可以用于封装可能返回错误码的旧式 C 风格 API,也可以与基于异常的现代 C++ 代码协同工作。通过 Expected
,可以逐步地将错误处理方式迁移到更现代、更类型安全的方法上,而无需对现有代码进行大规模的重构。
局限性 (Limitations):
① 学习曲线 (Learning Curve):对于初学者来说,理解 Expected
的概念和函数式操作可能需要一定的学习成本。特别是 andThen
等 Monadic 操作,可能需要一些时间才能掌握其精髓。然而,一旦理解了 Expected
的基本原理和用法,就会发现它带来的好处远大于学习成本。
② 代码冗余 (Code Redundancy):在某些简单的错误处理场景下,使用 Expected
可能会显得代码略微冗余。例如,如果一个函数只是简单地返回一个布尔值表示成功或失败,那么使用 Expected<void, ErrorCode>
可能不如直接返回 bool
或使用错误码简洁。因此,需要根据具体的场景权衡使用 Expected
的必要性。
③ 性能开销 (Performance Overhead):虽然 Expected
在某些情况下比异常性能更可预测,但它本身也存在一定的性能开销。例如,Expected
对象需要额外的空间来存储状态信息(是否有值或错误),并且在进行函数式操作时可能会产生临时的 Expected
对象,导致额外的拷贝或移动操作。在性能极其敏感的代码路径中,需要仔细评估 Expected
的性能影响,并进行必要的优化。
④ 错误类型选择 (Error Type Selection):Expected
的灵活性也带来了一个挑战,即如何选择合适的错误类型 E
。如果错误类型设计不当,可能会降低 Expected
的表达能力,甚至引入新的问题。例如,使用过于宽泛的错误类型(如 std::string
)可能会丢失错误的具体信息,而使用过于复杂的错误类型可能会增加代码的复杂性。因此,需要根据具体的应用场景仔细考虑错误类型的设计。
⑤ 并非所有错误场景都适用 (Not Suitable for All Error Scenarios):Expected
主要适用于可预期的、非致命的错误处理。对于那些表示程序严重错误或不可恢复状态的场景,例如内存耗尽、系统崩溃等,异常可能仍然是更合适的错误处理机制。此外,对于那些需要跨越函数调用栈进行错误处理的场景,异常的栈展开机制可能更自然和高效。
总而言之,Expected
是一个强大的错误处理工具,它在清晰性、类型安全性和函数式编程等方面具有显著的优势。然而,它并非银弹,也存在一些局限性。开发者应该根据具体的应用场景,权衡 Expected
的优势与局限性,并结合其他错误处理方法,选择最合适的方案来构建健壮、可靠的 C++ 代码。
7.2 Expected 的未来发展趋势 (Future Development Trends of Expected)
Expected
作为一种现代的错误处理方案,其发展前景广阔,并正在不断演进和完善。以下是一些 Expected
未来的发展趋势:
① 标准化 (Standardization):std::expected
的标准化是 Expected
最重要的发展趋势之一。C++ 标准委员会已经接受了 std::expected
的提案,并有望在未来的 C++ 标准版本中正式纳入。std::expected
的标准化将极大地推动 Expected
在 C++ 社区的普及和应用,使其成为事实上的标准错误处理方案之一。标准化不仅意味着更多的编译器和库将原生支持 Expected
,也意味着开发者可以更加放心地使用 Expected
,而无需担心兼容性问题。
② 更丰富的 API (Richer API):随着 Expected
的广泛应用,其 API 也在不断丰富和完善。未来可能会出现更多实用的成员函数和非成员函数,以满足各种复杂的错误处理需求。例如,可能会增加用于错误聚合、错误诊断、错误日志记录等方面的 API。此外,Expected
可能会与其他 Folly 库以及其他第三方库进行更深入的集成,提供更强大的功能和更便捷的使用方式。
③ 与协程 (Coroutines) 的结合 (Integration with Coroutines):协程是 C++20 引入的一项重要特性,它为异步编程带来了革命性的变化。Expected
与协程的结合将是未来发展的一个重要方向。通过 Expected
,可以更好地处理异步操作可能产生的错误,使得异步代码更加健壮和易于维护。例如,可以设计返回 Expected<T, E>
的协程,明确表示异步操作可能成功返回类型为 T
的值,或者失败返回类型为 E
的错误。
④ 性能优化 (Performance Optimization):虽然 Expected
在性能方面已经具有一定的优势,但仍然存在优化的空间。未来可能会在编译器层面和库层面进行更多的性能优化,例如减少不必要的拷贝、优化状态检查的实现、改进函数式操作的性能等。性能优化将使得 Expected
在更多性能敏感的场景中得到应用,进一步提升其竞争力。
⑤ 更广泛的应用场景 (Wider Application Scenarios):随着开发者对 Expected
的理解和应用不断深入,Expected
将会在更广泛的应用场景中得到应用。除了传统的后端服务、系统编程等领域,Expected
可能会在游戏开发、嵌入式系统、高性能计算等领域发挥重要作用。例如,在游戏开发中,可以使用 Expected
来处理资源加载失败、网络通信错误等问题;在嵌入式系统中,可以使用 Expected
来处理硬件故障、外设错误等问题。
⑥ 教育和推广 (Education and Promotion):为了更好地推广 Expected
的应用,需要加强相关的教育和推广工作。例如,可以编写更多的教程、示例代码、最佳实践指南等,帮助开发者快速上手和熟练使用 Expected
。此外,可以在各种技术会议、研讨会、博客等渠道宣传 Expected
的优势和应用场景,提高其知名度和影响力。
总而言之,Expected
的未来发展充满希望。随着标准化进程的推进、API 的不断完善、与其他技术的融合以及性能的持续优化,Expected
将会在 C++ 错误处理领域扮演越来越重要的角色,成为构建更健壮、更可靠的 C++ 代码的基石。
7.3 结语:构建更健壮的 C++ 代码 (Conclusion: Building More Robust C++ Code)
在本书的结尾,我们回顾了 folly/Expected.h
的方方面面,从其设计哲学、基本操作、函数式特性,到高级应用、API 解析、最佳实践以及未来的发展趋势。希望通过本书的系统性介绍,读者能够全面理解 Expected
的价值和用法,并将其应用到实际的 C++ 项目中。
Expected
的出现,不仅仅是提供了一种新的错误处理机制,更代表了一种现代 C++ 编程的理念:清晰、类型安全、函数式。它鼓励开发者在代码中显式地处理错误,而不是依赖隐式的异常或容易被忽略的错误码。它利用类型系统来保证错误处理的正确性,减少运行时错误的可能性。它借鉴函数式编程的思想,提供简洁、优雅的错误处理方式,提高代码的可读性和可维护性。
构建健壮的 C++ 代码,一直是软件开发领域的重要目标。Expected
正是实现这一目标的重要工具之一。通过合理地使用 Expected
,我们可以编写出更加可靠、易于理解、易于维护的 C++ 代码,减少错误,提高软件质量。
当然,Expected
并非万能的。在实际应用中,我们需要根据具体的场景,权衡各种错误处理方法的优缺点,选择最合适的方案。Expected
适用于处理可预期的、非致命的错误,尤其是在需要清晰地表达结果类型、进行链式操作和函数式编程的场景下。对于其他类型的错误,例如严重的系统错误或需要跨越函数调用栈处理的错误,可能需要结合异常或其他错误处理机制。
最后,希望读者能够将本书所学的知识应用到实践中,不断探索和创新,共同推动 C++ 技术的进步和发展。让我们一起努力,构建更健壮、更美好的 C++ 世界!🚀
END_OF_CHAPTER