【Modern】使用新特性重构代码

科技   2024-09-02 11:50   浙江  

今天来聊聊前几天看到的一个新特性:std::expected

传统上,c++开发人员在开发或实现一个函数的时候,往往要考虑到各种情况,才能编写出可靠且可维护的代码。但是基于以前语言特性的限制,往往需要使用错误码和异常等机制来管理错误,虽然这些方法有其优点,但它们也带来了一系列挑战,例如需要通过判断错误码来决定返回是否符合预期,如果错误码是符合预期的则需要采用其他方式获取函数的真实值等~

从一个例子入手

用一个简单的除法例子来入门吧。这得追溯到小学三年级,那时候老师会告诉我们:除数不能为0,所以在潜意识中,在做除法的时候,都会判断除数是否为0,我们往往会像如下这么做:

bool divide(int numerator, int denominator, int &result, std::string &err) {
if(denominator ==0.0){
    err ="divide by 0";
returnfalse;
}

  result = numerator / denominator;
returntrue;
}

int main() {
int a =1.0;
int b =0.0;

int c =1.0;

  std::string err;

bool r =divide(a, b, c, err);

if(!r){
    std::cout <<"err: "<< err <<"\n";
}else{
    std::cout <<"result: "<< result <<"\n";
}

return0;
}

上述实现确实有效,但其存在以下两个缺点:

缺乏类型安全:状态代码和错误消息相隔离,这可能存在处理不一致并增加出错的可能性可读性降低:错误处理的逻辑分散,使得代码难以阅读和维护

当然了,也可以像如下这么写:

int divide(int numerator, int denominator) {
  if (denominator == 0.0) {
    throw std::runtime_error("error: divide by 0");
  }
  
  return numerator / denominator;
}

这种方式使用了异常机制,虽然异常为错误处理提供了一种强大的机制,但也存在一系列挑战:

性能开销:由于堆栈展开和异常处理成本,异常可能会带来显著的开销,尤其是在性能敏感的程序中复杂性:处理异常可能会导致更复杂的代码,从而使得遵循错误处理逻辑变得更加困难

横空出世

那么,有没有一种更为优雅的方式,将预期输出和错误信息放在一个结构中,形如如下这种:

template <class_Ty,class_Err>
structexpected{
/*... lots of code ... */
_Ty_Value;
_Err_Unexpected;
};

其实,很明显,上述结构可行,但是,emm,浪费空间,所以此时,我们想到了Modern cpp中的另外一种新特性std::variant,结合上述两种特性,然后形如如下这种:

template<typename T, typename E>
using expected = std::variant<T, std::unexpected<E>>;

嗯,说实话,在接触此文的时候,没有去特意研究源码,只是在某些文章中提到了此种实现方案,感觉挺有意思,所以在此列出来~

好了,言归正传,正是基于前面传统实现的弊端,没有一个完美的解决方案,标准引入了一个新的方案来解决此类问题std::expected

提供了一种更轻量、更直接的替代方案,使错误处理变得明确,并减少了对复杂异常处理机制的需求提供了一种现代、类型安全的 C++ 错误处理方法,解决了传统方法的缺点(如返回代码和异常),通过将成功和错误状态封装在单个对象中,std::expected增强了代码的可读性、可维护性和性能,使其成为现代C++开发的必备工具允许开发人员在单个对象中表示值或错误,以干净、可读的方式简化成功和失败场景的处理

好了,我们现在使用std::expected来重写文章一开始的代码:

std::expected<int, std::string> divide(int numerator, int denominator) {
  if (denominator == 0.0) {
    return std::unexpected("error: divide by 0");
  }
  
  return numerator / denominator;
}

在上述这个实现中,divide接收两个参数,返回类型为std::expected<int, std::string>,如果分母为0,则返回std::unexpected,否则返回正确的结果。这种方法使错误处理变得明确而直接,从而增强了代码的可读性和可维护性。

成功与否

与std::optional使用方式一样,std::expected同样需要进行判断,一旦返回值使用了std::expected,我们就得判断函数的返回值内容包含的是一个有效值或者错误信息。为此,标准提供了两个成语函数以满足我们的需求:

has_value用以判断该函数的返回是否成功value用以获取返回值,即如果前面的has_value返回为true,则使用value获取值error用以获取错误信息,即如果前面的has_value返回为true,则使用error获取错误信息

下面是一个完整的使用示例:

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


std::expected<int, std::string> divide(int numerator, int denominator) {
if(denominator ==0.0){
return std::unexpected("error: divide by 0");
}

return numerator / denominator;
}

int main() {

auto r =divide(10,2);

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

auto er =divide(10,0);

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

return0;
}

在上述代码中,r和er均为std::expected类型的对象,通过has_value判断是否成功,如果成功,则使用value获取值,否则使用error获取错误信息。

除了上述最基本也就是最常用的功能外,标准也提供了其它几种方法:

**exp.and_then**:如果 exp 包含值,则返回指定函数调用的结果。如果 exp 为空,则返回一个空的 std::expected**exp.transform**:将 exp 中的值进行转换,返回包含转换后值的 std::expected。如果 exp 为空,则返回一个空的 std::expected**exp.or_else**:如果 exp 包含值,则返回 exp。如果 exp 为空,则返回指定函数的结果**exp.transform_error**:如果 exp 包含值,则返回 exp。如果 exp 为空,则返回一个包含转换后错误信息的新 std::expected

下面我们使用一个完整的例子来加深对这块的理解:

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

// 定义两个函数,分别返回 std::expected 和处理错误
std::expected<int, std::string> divide(int numerator, int denominator) {
if(denominator ==0){
return std::unexpected("Division by zero");
}
return numerator / denominator;
}

std::expected<int, std::string> add_ten(int value) {
return value +10;
}

int main() {
auto result1 =divide(20,4).and_then(add_ten);
if(result1){
        std::cout <<"Result1: "<<*result1 << std::endl;
}else{
        std::cout <<"Error1: "<< result1.error()<< std::endl;
}

auto result2 =divide(20,5).transform([](int value){return value *2;});
if(result2){
        std::cout <<"Result2: "<< result2.value()<< std::endl;
}else{
        std::cout <<"Error2: "<< result2.error()<< std::endl;
}

auto result3 =divide(20,0).or_else([](const std::string&err){return std::expected<int, std::string>(-1);});
if(result3){
        std::cout <<"Result3: "<< result3.value()<< std::endl;
}else{
        std::cout <<"Error3: "<< result3.error()<< std::endl;
}


auto result4 =divide(20,0).transform_error([](const std::string& error){
return error +" - Please provide a non-zero denominator.";
});

if(result4){
        std::cout <<"Result4: "<< result4.value()<< std::endl;
}else{
        std::cout <<"Error4: "<< result4.error()<< std::endl;
}

return0;
}

上述代码输出结果如下:

Program returned: 0
Program stdout
Result1: 15
Result2: 8
Result3: -1
Error4: Division by zero - Please provide a non-zero denominator.


推荐阅读  点击标题可跳转

1、《黑神话·悟空》是用什么编程语言开发的?

2、知乎热议:为什么多数程序员都不做个人独立开发?

3、free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?

CPP开发者
我们在 Github 维护着 9000+ star 的C语言/C++开发资源。日常分享 C语言 和 C++ 开发相关技术文章,每篇文章都经过精心筛选,一篇文章讲透一个知识点,让读者读有所获~
 最新文章