• C++中std::expected特性的高级用法
  • 发布于 2个月前
  • 232 热度
    0 评论
std::expected是C++23中引入的一项强大特性,它为传统的错误处理方法提供了一种现代化、类型安全的替代方案。在我作为开发者的经历中,错误处理一直是编写健壮且可维护代码的关键方面。不同语言的错误处理最佳实践各有不同且不断发展演变。传统上,C++开发者依靠返回码和异常等机制来管理错误。尽管这些方法有其优点,但也存在各自的挑战,比如在对性能要求苛刻的应用程序中处理异常,或者确保通过返回码充分传递错误信息等问题。

std::expected允许开发者在单个对象中表示一个值或者一个错误,以简洁且易读的方式简化了成功和失败场景的处理。这一实用类型提高了代码的清晰度和可维护性,并且通过提倡显式的错误处理,在不必要使用异常的情况下减少对异常的依赖,符合现代C++实践。

为何现代C++开发者需要接纳std::expected
C++23标准库中包含std::expected解决了与异常和返回码相关的多个痛点,它提供了一种更通用、更具表现力的解决方案。以下是std::expected成为C++开发者变革因素的原因:
• 类型安全的错误处理:std::expected通过清晰地区分成功结果和错误状态来强制实现类型安全,降低了运行时错误的风险。
• 增强代码可读性:借助std::expected,错误处理的意图在代码中得以明确体现,使其更易于理解和维护。
• 性能提升:与可能产生显著开销的异常不同,std::expected提供了一种更轻量级的替代方案,适用于对性能敏感的应用程序。
• 更高的灵活性:std::expected能够与现有代码库无缝集成,为逐步实现错误处理的现代化提供了一条途径,无需完全重写代码。
无论你是在开发高性能应用程序、编写健壮的库,还是维护遗留代码,理解并运用std::expected都有助于打造更可靠、更易于维护的代码库。

std::expected是什么?
• 定义与目的
std::expected是C++23标准库中的新成员,旨在提供一种类型安全的方式来表示可能成功或失败的操作结果。与诸如异常或错误码等传统错误处理技术不同,std::expected将有效结果或错误封装在单个对象内。这种二元性使得开发者能够明确且简洁地处理错误,有助于编写出更易读、更易维护的代码。

本质上,std::expected<T, E>是一个模板类,它表示类型为T的预期值或者类型为E的意外错误。std::expected的引入满足了C++中对更现代、更具表现力的错误处理方法的需求,使得编写健壮软件时无需面对异常所带来的复杂性和开销。

• 动机与优势
std::expected背后的主要动机是通过以下方式简化错误处理:
• 促进类型安全:确保错误被显式处理,防止可能导致未定义行为的未检查错误。
• 增强代码可读性:清晰直白的错误处理逻辑提升了代码的整体可维护性。
• 减少开销:为异常提供了一种轻量级的替代方案,这在对性能要求苛刻的应用程序中尤为有益。
通过使用std::expected,开发者能够避开传统错误处理方法的陷阱,采用一种更一致、更可预测的方式来管理应用程序中的成功和失败情况。

• 关键特性
• 类型安全表示:使用显式类型区分成功和错误状态。
• 统一的错误处理:将成功和错误处理整合到一个统一的机制中。
• 轻量级且高效:通过避免栈展开的开销,相较于异常具有性能优势。

• 灵活且可扩展:易于与现有代码库集成,并支持广泛的使用场景。


代码示例:使用std::expected
让我们通过一个实际示例来看看std::expected是如何工作的,并将其与传统错误处理方法进行比较。
• 示例:使用std::expected的除法函数
std::expected<double, std::string> divide(double numerator, double denominator) {
    if (denominator == 0.0) {
        return std::unexpected("Error: Division by zero");
    }
    return numerator / denominator;
}
在这个示例中,divide函数使用std::expected<int, std::string>来表示有效的除法结果或者错误消息。如果除数不为零,函数返回有效的结果;否则,返回错误消息。这种方法使得错误处理变得明确且直观,增强了代码的可读性和可维护性。
• 与其他错误处理方法的比较
• 与返回码的比较
传统上,函数可能会返回一个状态码来指示成功或失败,通常还需要单独的机制来传递错误细节。
// 使用std::expected
std::expected<double, std::string> divide_expected(double numerator, double denominator) {
if(denominator ==0.0){
return std::unexpected("Error: Division by zero");
}
return numerator / denominator;
}
// 堆代码 duidaima.com
// 使用返回码
bool divide_return_code(double numerator, double denominator, double& result, std::string& error) {
if(denominator ==0.0){
        error ="Error: Division by zero";
returnfalse;
}
    result = numerator / denominator;
returntrue;
}
虽然这种方法可行,但存在几个缺点:
• 缺乏类型安全:状态码和错误消息是分离的,这可能导致处理不一致,并增加出错的可能性。
• 可读性降低:错误处理逻辑分散,使得代码更难阅读和维护。
std::expected通过提供一种统一且类型安全的方法来处理结果和错误,解决了这些问题。

• 与异常的比较
另一种常见的错误处理方法是使用异常。
// 使用std::expected
std::expected<double, std::string> divide_expected(double numerator, double denominator) {
if(denominator ==0.0){
return std::unexpected("Error: Division by zero");
}
return numerator / denominator;
}

// 使用异常
double divide_exception(double numerator, double denominator) {
if(denominator ==0.0){
throw std::runtime_error("Error: Division by zero");
}
return numerator / denominator;
}
虽然异常为错误处理提供了一种健壮的机制,但也存在一系列挑战:
• 性能开销:由于栈展开和异常处理成本,异常在对性能要求苛刻的应用程序中可能会引入显著的开销。
• 复杂性:处理异常可能导致代码更复杂,使得错误处理逻辑更难理解。
std::expected提供了一种更轻量级且更直观的替代方案,使错误处理更明确,并减少了对复杂异常处理机制的需求。

std::expected为C++中的错误处理提供了一种现代化、类型安全的方法,解决了像返回码和异常等传统方法的不足之处。通过将成功和错误状态封装在单个对象内,std::expected增强了代码的可读性、可维护性和性能,使其成为现代C++开发的必备工具。

std::expected的基本用法
• 创建std::expected对象
使用std::expected首先要创建一个能容纳值或错误的对象。类型std::expected<T, E>表示类型为T的预期值或者类型为E的错误。以下是一个简单示例来说明基本用法。
// 执行除法并使用std::expected处理错误的函数
std::expected<int, std::string> safe_divide(int numerator, int denominator) {
  if (denominator == 0) {
    return std::unexpected("Error: Division by zero");
  }

  return numerator / denominator;
}
在这个示例中,safe_divide函数接收两个整数并返回一个std::expected<int, std::string>。如果分母为零,函数使用std::unexpected返回一条错误消息;否则,返回除法的结果。

• 检查成功或错误
一旦有了std::expected对象,就需要检查它包含的是有效值还是错误。has_value()方法可用于确定操作是否成功,而value()和error()方法分别允许访问包含的值或错误。
#include <iostream>
#include <expected>
#include <string>

// 执行除法并使用std::expected处理错误的函数
std::expected<int, std::string> safe_divide(int numerator, int denominator) {
if(denominator ==0){
return std::unexpected("Error: Division by zero");
}
return numerator / denominator;
}

int main() {
// 示例:检查并访问std::expected
auto result =safe_divide(10,2);

// 检查
if(result.has_value()){
        std::cout <<"Result: "<< result.value()<<'\\n';
}else{
        std::cout << result.error()<<'\\n';
}
auto errorResult =safe_divide(10,0);
// 检查
if(errorResult.has_value()){
        std::cout <<"Result: "<< errorResult.value()<<'\\n';
}else{
        std::cout << errorResult.error()<<'\\n';
}

return0;
}
在这段代码中,result和errorResult是std::expected<int, std::string>对象。has_value()方法用于检查除法是否成功,value()或error()则用于获取结果或错误消息。

• 使用std::expected转换和访问结果
std::expected提供了一些有用的方法,能以安全且富有表现力的方式对其内容进行转换和访问,这些方法包括and_then(用于链式操作)和transform(用于修改值)。
#include <iostream>
#include <expected>
#include <string>
#include <cmath>

// 执行除法并使用std::expected处理错误的函数
std::expected<int, std::string> safe_divide(int numerator, int denominator) {
if(denominator ==0){
return std::unexpected("Error: Division by zero");
}

return numerator / denominator;
}

// 对数字进行平方的函数
int square(int value) {
return value * value;
}

// 使用and_then链式操作,对成功除法的结果进行平方的函数
std::expected<int, std::string> square_if_success(int numerator, int denominator) {
returnsafe_divide(numerator, denominator).and_then([](int value){
return std::expected<int, std::string>(square(value));
});
}

int main() {
// 示例:使用and_then和transform
auto result =square_if_success(10,2);

// 检查
if(result.has_value()){
        std::cout <<"Squared Result: "<< result.value()<<'\\n';
}
else{
        std::cout << result.error()<<'\\n';
}

auto errorResult =square_if_success(10,0);

// 检查
if(errorResult.has_value()){
        std::cout <<"Squared Result: "<< errorResult.value()<<'\\n';
}
else{
        std::cout << errorResult.error()<<'\\n';
}

// 使用transform对存在的值应用转换
auto transformedResult =safe_divide(10,2).transform([](int value){
return value +1;
});
// 检查
if(transformedResult.has_value()){
        std::cout <<"Transformed Result: "<< transformedResult.value()<<'\\n';
}
else{
        std::cout << transformedResult.error()<<'\\n';
}

return0;
}
在这段代码中,square_if_success函数使用and_then链式操作对成功除法的结果进行平方。transform方法同样可用于在值存在的情况下对其应用转换。
std::expected为C++中的错误处理提供了一种强大且类型安全的方式。通过使用has_value()、value()、error()、and_then和transform等方法,能够更优雅地处理成功和错误状态,使代码更简洁、更易于维护。这一基本用法部分涵盖了如何创建和操作std::expected对象、检查成功或错误、安全地访问值以及有效地处理错误,为更高级的使用场景奠定了坚实基础。

std::expected的高级用例
• 使用std::expected链式操作
std::expected的一个强大功能是能够以单子(monadic)风格链式操作,从而实现清晰简洁的错误传播和处理。这是通过and_then和or_else方法实现的。
• and_then:此方法用于链式操作,只有当std::expected包含有效值时才会执行后续操作,有助于在一系列操作中传递成功值。
• or_else:此方法用于在std::expected包含错误时提供替代操作或错误处理路径。
#include <iostream>
#include <expected>
#include <string>

// 执行除法并使用std::expected处理错误的函数
std::expected<int, std::string> safe_divide(int numerator, int denominator) {
if(denominator ==0){
return std::unexpected("Error: Division by zero");
}
return numerator / denominator;
}

// 给数字加5的函数
std::expected<int, std::string> add_five(int value) {
return value +5;
}

// 对数字进行平方的函数
std::expected<int, std::string> square(int value) {
return value * value;
}

int main() {
// 示例:使用and_then链式操作
auto result =safe_divide(20,2)
.and_then(add_five)
.and_then(square);

// 检查
if(result.has_value()){
        std::cout <<"Final Result: "<< result.value()<<'\\n';
}else{
        std::cout << result.error()<<'\\n';
}

// 示例:使用or_else处理错误
auto errorResult =safe_divide(20,0)
.and_then(add_five)
.and_then(square)
.or_else([](std::string error){
            std::cerr <<"Error occurred: "<< error <<'\\n';
return std::expected<int, std::string>(0);// 提供默认值
});

// 检查
if(errorResult.has_value()){
        std::cout <<"Handled Result: "<< errorResult.value()<<'\\n';
}else{
        std::cout << errorResult.error()<<'\\n';
}

return0;
}
在这个示例中,操作通过and_then链式连接在一起,并且每一步只有在前一步成功的情况下才会执行。如果任何操作失败,链就会中断,错误会被传播下去。

• 转换值和错误
std::expected允许使用transform和transform_error方法分别对值和错误进行转换。
• transform:此方法在值存在的情况下对其应用转换,错误保持不变。
• transform_error:此方法在错误存在的情况下对其应用转换,值保持不变。
#include <iostream>
#include <expected>
#include <string>

// 执行除法并使用std::expected处理错误的函数
std::expected<int, std::string> safe_divide(int numerator, int denominator) {
if(denominator ==0){
return std::unexpected("Error: Division by zero");
}
return numerator / denominator;
}

int main() {
// 示例:使用transform和transform_error
auto result =safe_divide(10,2)
.transform([](int value){
return value * value;// 如果值存在,对其进行平方
});

// 检查
if(result.has_value()){
        std::cout <<"Transformed Result: "<< result.value()<<'\\n';
}else{
        std::cout << result.error()<<'\\n';
}

auto errorResult =safe_divide(10,0)
.transform_error([](const std::string& error){
return error +" - Please provide a non-zero denominator.";// 给错误消息添加详细信息
});

// 检查
if(errorResult.has_value()){
        std::cout <<"Result: "<< errorResult.value()<<'\\n';
}else{
        std::cout << errorResult.error()<<'\\n';
}

return0;
}
在这个示例中,transform用于对除法结果进行平方,transform_error用于在出现错误时给错误消息添加额外的详细信息。

• std::expected的高级技巧
示例:使用std::expected进行错误聚合
std::expected还可用于聚合来自多个操作的错误,并提供综合的错误报告。
#include <iostream>
#include <expected>
#include <vector>
#include <string>

// 验证单个输入的函数
std::expected<void, std::string> validate_input(int input) {
if(input <0){
return std::unexpected("Error: Negative value not allowed");
}

if(input >100)
{
return std::unexpected("Error: Value exceeds maximum limit");
}

return{};
}

// 验证多个输入并聚合错误的函数
std::expected<void, std::vector<std::string>>validate_inputs(const std::vector<int>& inputs){
    std::vector<std::string> errors;
for(constauto& input : inputs){
auto result =validate_input(input);
if(!result){
            errors.push_back(result.error());
}
}

// 检查
if(!errors.empty()){
return std::unexpected(errors);
}

return{};
}

int main() {
    std::vector<int> inputs ={10,-5,150,20};

// 示例:使用std::expected进行错误聚合
auto result =validate_inputs(inputs);

// 检查
if(result.has_value()){
        std::cout <<"All inputs are valid.\\n";
}else{
        std::cout <<"Errors:\\n";
for(constauto& error : result.error()){
            std::cout <<"- "<< error <<'\\n';
}
}

return0;
}
在这个示例中,validate_inputs函数将错误聚合到一个向量中,并将其作为单个std::expected对象返回。这样能够以简洁明了的方式实现全面的错误报告。
std::expected为C++中高级错误处理提供了一套通用且强大的工具集。通过利用and_then、transform以及transform_error等方法,开发者能够编写出简洁、健壮的代码。它与遗留代码集成的能力以及潜在的性能优势,使其成为现代C++开发的必备工具。诸如错误聚合这样的高级技巧进一步拓展了它的实用性,展示了其在各种场景下的灵活性和有效性。

以上就是我的分享。这些分析皆源自我的个人经验,希望上面分享的这些东西对大家有帮助,感谢大家!
用户评论