今天来聊聊前几天看到的一个新特性: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.