错误处理方法一直是个头疼的问题,最近在写程序的时候得出一套使用exception的原则,作为心得记录下来
1.使用对象管理资源
首先对于要释放的资源先用对象来管理,利用c++的释构机制来避免资源泄露,比如WIN32的句柄都需要在用完后关闭
protected :
string m_filepath;
HANDLE m_file;
public :
File( const string filepath);
~ File();
};
File::File( const string filepath){
m_filepath = filepath;
m_file = OpenFile(...);
if (INVALID_FILE == m_file)
throw runtime_error( " Open file failed. " );
}
File:: ~ File(){
CloseHanle(m_file);
}
这样就可以将资源的释放交给释构函数来完成,在抛异常的时候就无后顾之忧了.
2.异常对象
c++语言本身带了一套异常体系,根类是 exception ,子类分别有 runtime_error ,logic_error ,invalid_argument 等...,一些大型的软件会自己定义一套异常体系,但作用也是跟c++的异常对象差不多,可能会根据需要扩充一些方法,但我认为一般软件使用c++的就足够了。有些软件还会定义带有消息栈的异常类,初衷可能是为了尽可能详细地描述异常,我认为这其实是不必要的,调用者只需要知道他调用的方法是不是成功就行了,不需要知道再内层发生了什么错误(信息隐藏),再内层发生的错误应该由内层处理掉,不应该再流出来,至于异常消息,应该随着调用盏的层层展开层层输出而不是留到最外面才printStackTrace()...,这个问题下面再讨论。
3.总是建立有效的对象
就是一个对象如果他构造成功,则保证他是完整的,状态是正确的,像上面那个File类,如果构造函数中的OpenFile失败了,就抛出异常阻止这个对象的创建,这样就保证了如果File这个对象构成成功,则他一定是打开了某个文件,可以进行读写,而不会说File构造成功了,但文件却没有打开,以后调用写函数的时候还会出现File not open这种情况。而且释构函数不需要
if(m_file)
CloseHandle(m_file);
因为对象必定是有效的!
但要维持对象必须是有效的这种状态并不简单,有时在写成员函数的时候出错可能会破坏对象的状态,所以要还要用exception free的思维写程序,参见 《exceptional c++》。
4.无异常则业务就是成功的
如果一个函数在业务意义上是有结果的,则用返回值来传递结果并用异常来传递错误
如果一个函数在业务意义上是无结果的,则返回void并用异常来传递错误
不要再出现用返回错误代码或者一个无效对象来指定操作不成功的情况
比如说File对象有一个成员函数要获取文件的大小
try {
fseek(SEEK_END, 0 ,...);
size_t s = ftell(....);
if ( - 1 == s)
throw runtime_error( " ftell failed " );
return s;
}
catch (...){
// return -1; // 不要这样 !!!
throw runtime_error( " Failed to get file size: Unknown exception. " ); // 应该这样
}
}
或者说
try {
// 执行加入用户的操作,有问题就抛出异常
}
catch (...){
throw runtime_error( " Failed to add user: Unknown exception " );
}
}
则外面的调用者只需要
try {
UserManager userManager
userManager.addUser(User( " Lingch " ));
// 如果程序成功运行到这里,则必定是一切正常,无须判断任何返回的错误,新用户"Lingch"必定已经成功加入了。
// 因为如果加入用户失败,程序早就到下面的catch里面去了。
// Do other process...
}
catch (exception & ex){
cerr << ex.what() << endl;
throw runtime_error( " Do business1 failed. " );
}
catch (...){
// ...
}
}
这样try块里面的代码就会很干净,只包含业务代码,异常全部流到catch块里面去处理。
5.异常处理
在try块中只处理业务,所有异常留到catch块中来处理,利用exception的message来传递异常信息
try {
LOG4CPLUS_INFO(logger, " Start adding user. " );
LOG4CPLUS_DEBUG(logger, " Adding user: step 1. " );
// ...第一步,假设没有异常
LOG4CPLUS_DEBUG(logger, " Adding user: step 2. " );
// ...第二步,假设出现异常,并抛出了runtime_error对象
LOG4CPLUS_DEBUG(logger, " Adding user: step 3. " );
// ...第三步,如果第二步出现异常,这里应该不会再执行
LOG4CPLUS_INFO(logger, " Adding user done. " );
}
catch (exception & ex){ // 会扑获第二步时抛出的runtime_error对象
LOG4CPLUS_ERROR(logger,ex.what()); // 这是写入到日志的,要写具体错误原因,
// 也就是第二步抛出异常时传递过来的信息,
// 这样日志中就会反映出业务(try块内)到底发生了什么错误,
// 层层展开就会形成消息栈(所以说带消息栈的异常是没什么作用的)
// 当然日志不一定是文件,也可以是控制台输出
throw runtime_error( " Failed to add user. " ); // 这是报告给调用者的,无须写具体错误原因
// 只需要尽分内事知会调用者本业务失败就行了
}
catch (...){
LOG4CPLUS_ERROR(logger, " Unknown exception " );
throw runtime_error( " Failed to add user. " );
}
}
为什么在catch块中throw runtime_error不需要让外部调用者知道到底发生了什么错误?因为如果exception是由本层抛出的,则只能够反映本层发生了一些错误;如果是内层抛出的并且本层能够处理并恢复的,则应该在第二步中套嵌一个try块处理掉并恢复过来而不会流到catch里面去;如果是内层抛出并且无法处理的,那反映内层发生了什么错误是没有意义的,难道外层的调用者还得关注n层以内是否被0除这种异常?:-)反正本层的代码只报告本层成功与否,而不要再报告内层的状态,内层的异常应该在本层处理掉而不能流向外层,这样各司其职层层套嵌地隐藏内层应该作为一个原则,避免跨越调用层次的异常传递。
6.效果
通过这样的处理,如果有异常出现,日志里面应该会出现异常消息栈,而每个函数的try块内只包含干净的业务代码,所有的异常都由catch块来处理了:-)
其中我认为最困难的地方可能就是怎样保证一个对象是 exception free的
另外就是使用exception的效率问题