前言
这篇文章是在手机上写的,如果你没在手机上写这种文章,可能无法体会其中的滋味,那叫一个蛋疼。不止是这篇文章,我最近的几个项目全是在手机上编写。并不是我想这样,是因为机器坏了,在新的机器来临之前,我什么都做不了,一天不编程我都不自在,所以我还得忍受这种煎熬一段时间。
我建议看这篇文章的人,如果有时间,可以尝试在手机上编程,不去碰电脑就只用手机。除了受到煎熬之外,你能获得很多东西,那就是对电脑的怀念与尊重。你会怀念那宽大的显示屏,怀念那极具人性化的大键盘,怀念那熟悉的台式操作系统,怀念那熟悉的编辑器/IDE,怀念那熟悉的调试器。当再回到PC时,你会发现这一切有多美好。虽然目前我还没有回来,但我已经能感受到了。
棘手的错误处理
错误处理是每个程序必须具备的工作,但是错误处理是极其繁琐和困难的,软件设计领域发展了几十年,但直到今天错误处理都是一个超难的学术级问题。
这里我们不深入错误处理,只来到最基本的地方:判断是否出错。一般来说,目前我们有两种判断方式,一种是函数通过返回值来告诉调用者有没有出错,另一种是使用异常。这里我们不使用异常方式,而只使用最广泛的判断返回值方式。
这是一个常见的错误处理例子:
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
use namespace std;
string GetFileContent(const string& path, string& result)
{
ifstream f(path);
if (f.is_open())
{
stringstream ss;
ss << f.rdbuf();
string fuck = ss.str();
if (fuck.size() > 0)
{
content = fuck;
return {};
}
else
{
return "this is a zero size fuck file.";
}
}
else
{
return "cannot open this fuck file.";
}
}
bool IsValidContent(const string& content)
{
bool is_valid = (string::npos == content.find("c++")) &&
(string::npos == content.find("C++"));
return is_valid;
}
int main(int argc, char* args[])
{
if (argc < 2)
{
cout << "usage: fuckc++ <filepath>" << endl;
}
else if (argc > 2)
{
cout << "please do'not make die." << endl;
}
else
{
string content;
string e = GetFileContent(args[1], content);
if (e.size() > 0)
{
cout << "error: " << endl;
}
else
{
if (IsValidContent(content))
{
cout << "content:" << endl;
cout << content << endl;
}
else
{
cout << "error: this is a fuck file." << endl;
}
}
}
return 0;
}
可以看到,主要在GetFileContent函数上面,我们需要判断文件是否正确获取。GetFileContent函数接受一个路径和结果参数,如果正确完成任务就设置结果,如果有问题就返回一个错误信息。
这种函数的问题是,你每次都需要一次额外的赋值(调用函数,接收错误信息)。在小项目中这没什么,但在大项目中就不好维护了。
合并执行状态与返回值
那么我们怎么解决这个问题呢?错误肯定还是要处理的,错误信息肯定也是要接收的。但是我们发现这被分成两个语句,首先调用函数接收错误信息,然后判断有没有错误。那么能不能合并呢?当然可以,我们先建立一个专门用作返回值的泛型结构:
// 需要引用<functaional>
template <typename T>
struct RESULT
{
RESULT():_f([](T&)->bool{return false;}){}
RESULT(const T& a):_f([=](T& a_)->bool{a_ = a;return true;}){}
bool operator[](T& a)
{
return _f(a);
}
private:
function<bool(T&)> _f;
};
这样的话,我们就可以这样写:
RESULT<int> TestOk()
{
return {1};
}
RESULT<int> TestError()
{
return {};
}
void Test()
{
int a;
if (TestOk()[a])
{
cout << "a = " << a << endl;
}
if (!TestError()[a])
{
cout << "function run failed." << endl;
}
}
// 程序输出
// a = 1
// function run failed.
可以看到,我们将错误判断和获取函数结果合并在了一起,这么做好处在哪?那就是将错误信息与函数返回值分离了,同时又将调用函数,判断是否出错,接受函数返回值合并到一起,我们使用一个if就可以完成整套操作。这只适用于仅有成功失败两种状态的函数,那么带有错误信息的函数也可以合并吗?当然可以,不过在此之前我们先看一个额外的东西。
*此处我使用[]作为获取函数返回值的方法,这只是我个人的喜好。一般来说使用()更好。
多返回值
template <typename T1, typename T2>
struct RESULT2
{
RESULT2():_f([](T1&, T2&)->bool{return false;}){}
RESULT2(const T1& a, const T2& b)
:_f([=](T1& a_, T2& b_)->bool{a_ = a; b_ = b; return true;}){}
bool operator()(T1& a, T2& b)
{
return _f(a, b);
}
private:
function<bool(T1&, T2&)> _f;
};
RESULT2<int, float> TestOk2()
{
return {2, 0.618};
}
RESULT2<int, float> TestError2()
{
return {};
}
void Test()
{
int a;
float b;
if (TestOk()(a, b))
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
if(!TestError()(a, b))
{
cout << "function run failed." << endl;
}
}
// 程序输出
// a = 1
// b = 0.618
// function run failed.
这是支持多返回值的方法,但是为什么要额外定义一个名称RESULT2呢?不能只有一个RESULT吗?当然可以,不过实现方法要复杂很多,此文不会深入到那里,因为多返回值的需求不高。
带错误信息的合并
一开始的例子中,GetFileContent返回不只是成功和失败两种状态,而是具体的错误信息。显然我们上面的方式无法应付这种情况。所以我们需要这么做:
struct ERROR
{
ERROR(){}
ERROR(const string& info)
:_info(info){}
const string& Info() const
{
return _info;
}
private:
string _info;
};
template <typename T>
struct RESULT
{
RESULT(const ERROR& e):_f([=](ERROR& e_, T&)->bool{e_ = e;return false;}){}
RESULT(const T& a):_f([=](ERROR&, T& a_)->bool{a_ = a;return true;}){}
bool operator()(ERROR& e, T& a)
{
return _f(e, a);
}
private:
function<bool(ERROR&, T&)> _f;
};
RESULT<int> TestOk()
{
return {1};
}
RESULT<int> TestError()
{
return {ERROR("this is a error info.")};
}
void Test()
{
ERROR e;
int a;
if (TestOk()(e, a))
{
cout << "a = " << a << endl;
}
if (!TestError()(e, a))
{
cout << "error: " << e.Info() << endl;
}
}
// 程序输出
// a = 1
// error: this is a error info.
很好,那么现在,我们成功的实现了一个合并错误判断与获取返回值以及分离错误信息与返回值的这么一个玩意。这个玩意的用处在哪里?需要错误判断的地方。这玩意有什么好处?不见得有多好,但如果项目不使用异常,那么这种方法比任何一种传统的判断方式都要优雅也更好维护。
后记
这个玩意是我今天早上试验出来的,虽然我自己设计的,但基本的套路肯定很多年前就有了,只不过现在有了c++11,可以弄的更优雅而已。当然,我自己的项目用的是c++14。
在我的项目中,是使用异常的,但是我发现,异常应该只真正的异常,当然我一开始就考虑了这一点。我首先定义了一个异常基类Exception,然后定义了一个子类Error,将真正的异常(软件缺陷,系统故障,内存耗尽等难预料的问题)与错误(用户输入不正确,访问越界等等)分开。但即便如此也还是有问题,因为异常机制本身就有问题。会扰乱程序的执行流程,可读性也会戏剧性的降低,后期对代码的理解也会变得困难。如果是大型项目,这必然会导致对代码的维护极其困难。当然如果只用异常机制处理真正的异常,并不会有这么恐怖的结果,不过依然会很棘手。
(由于异常机制本身的原因,如果想要细致的区分各种异常与错误,就得定义一大堆异常类。我认为这很不好,因为类是一个很恐怖的东西,特别是在C++中。一个使用错误ID的系统和一个使用异常的系统,两者的可维护性可能有成百上千倍的差别,这不是开玩笑)
所以我才自己寻找另一种处理错误的方式,当然目前也只是实现最基本的东西:对错误的优雅判断。
*第一个例子是写文章时临时弄的,没有经过编译验证。
谢谢观看,最后祝大家提前健康。