如何处理C++构造函数中的错误——兼谈不同语言的错误处理
用C++写代码的时候总是避免不了处理错误,一般来说有两种方式,通过函数的返回值或者抛出异常。C语言的错误处理一律是通过函数的返回值来判断的,一般是返回0、NULL或者-1表示错误,或者直接返回错误代码,具体是哪种方式没有统一的规定,各种API也各有各的偏好。譬如fopen函数,当成功时返回文件指针,失败时返回NULL,而POSIX标准的open函数则在成功时返回0或者正数,失败时返回-1,然后需要再通过全局变量errno来判断具体错误是什么,配套的还有一系列perror、strerror这样的函数。
C++的错误处理方式
C++号称向下兼容C语言,于是就将C语言通过返回值的错误处理方式也搬了进来。但C++最大的不同是引入了异常机制,可以用throw产生一个异常,并通过try和catch来捕获。于是就混乱了,到底是什么时候使用返回值表示错误,什么时候使用异常呢?首先简单谈论一下异常和返回值的特点。
异常的优点
- 错误信息丰富,便于获得错误现场
- 代码相对简短,不需要判断每个函数的返回值
异常的缺点
- 使控制流变得复杂,难以追踪
- 开销相对较大
返回值的优点
- 性能开销相对小
- 避免定义异常类
返回值的缺点
- 程序员经常「忘记」处理错误返回值
- 每个可能产生错误的函数在调用后都需要判断是否有错误
- 与「真正的」返回值混用,需要规定一个错误代码(通常是
0、-1或NULL)
使用异常还是返回值
我的观点是,用异常来表示真正的、而且不太可能发生的错误。所谓不太可能发生的错误,指的是真正难以预料,但发生了却又不得不单独处理的,譬如内存耗尽、读文件发生故障。而在一个字符串中查找一个子串,如果没有找到显然应该是用一个特殊的返回值(如-1),而不应该抛出一个异常。
一句话来概况就是不要用异常代替正常的控制流,只有当程序真的「不正常」的时候,才使用异常。反过来说,当程序真正发生错误了,一定要使用异常而不是返回一个错误代码,因为错误代码总是倾向于被忽略。如果要保证一个以返回值来表示错误代码的函数的错误正确地向上传递,需要在每个调用了可能产生错误的函数后面都判断一下是否发生了错误,一旦发生了不可解决的错误,就要终止当前函数(并释放当前函数申请的资源),然后向上传递错误。这样一来错误处理代码会被重复地写好几遍,十分冗杂,譬如下面代码:
int func(int n) {
int fd = open("path/to/file", O_RDONLY);
if (fd == -1) {
return ERROR_OPEN;
}
int* array = new[n];
int err;
err = do_something(fd, array);
if (err != SUCCESS) {
delete[] array;
return err;
}
err = do_other_thing();
if (err != SUCCESS) {
delete[] array;
return err;
}
err = do_more_thing();
if (err != SUCCESS) {
delete[] array;
return err;
}
delete[] array;
return SUCCESS;
}
对使用异常容易增加函数出口的指控其实是不成立的,因为即使使用返回值,这些出口也是免不了的,除非程序员有意或无意忽略掉,但异常是不可忽略的。如果你认为可以把判断错误的if语句缩写到一行使代码变得「更清晰」,那么我只能说是自欺欺人。
有些错误几乎总是可以被立即恢复(譬如前面所说的查找一个字符串不存在的子串,甚至都不能说这是一个「错误」),而且返回值本身就传递一定信息,就不需要使用异常了。
鉴于C++没有统一的ABI,并不建议在模块的接口上使用异常。如果要使用,就要把可能曝露给用户的异常全部声明出来,不要把其他类型的异常丢给用户去处理,尤其是内部状态——模块的使用者通常也不会关心模块内部具体是哪条语句发生错误了。
构造函数中的错误
有一个相当实际的问题是,如何处理构造函数的错误?我们都知道构造函数是没有返回值的,怎么办呢?通常有三种常见的处理方法,标记错误状态、使用一个额外的initialize函数来初始化,或者直接抛出异常。
合格的C++程序员都知道C++的析构函数中不应该抛出异常,一旦析构函数中的异常没有被捕获,整个程序都要被中止掉。于是许多人就对在构造函数中抛出异常也产生了对等的恐惧,宁可使用一个额外的初始化函数在里面初始化对象的状态并抛出异常(或者返回错误代码)。这样做违背了对象产生和初始化要在一起的原则,强迫用户记住调用一个额外的初始化函数,一旦没有调用直接使用了其他函数,其行为很可能是未定义的。
使用初始化函数的惟一好处可能是避免了手动释放资源(释放资源的操作交给析构函数来做),因为C++的一个特点是构造函数抛出异常以后析构函数是不会被调用的,所以如果你在构造函数里面申请了内存或者打开了资源,需要在异常产生时关闭。但想想看其实并不能完全避免,因为有些资源可能是要在可能产生错误的函数调用过后才被申请的,还是无法完全避免手工的释放。
标记错误状态也是一种常见的形式,譬如STL中的ifstream类,当构造时传入一个无法访问的文件作为参数,它不会返回任何错误,而是标记的内部状态为不可用,用户需要手工通过is_open()函数来判断是否打开成功了。同时它还有good()、fail()两个函数,同时也重载了bool类型转换运算符用于在if语句中判断。标记状态的方法在实践中相当丑陋,因为在使用前总是需要判断它是否「真的创建成功了」。
最直接的方法还是在构造函数中抛出异常,它并不会向析构函数中抛出异常那样有严重的后果,只是需要注意的是抛出异常以后对象没有被创建成功,析构函数也不会被调用,所以应该自行把申请的资源全部都释放掉。
如何在构造函数中捕获异常
构造函数与普通函数有一个很不一样特性,就是构造函数可以有初始化列表,例如下面的代码:
class B {
public:
B(int val) : val_(val * val) {
}
private:
int val_;
};
class A {
public:
A(int val) : b_(val) {
a_ = val;
}
private:
int a_;
B b_;
};
以上的代码中A的构造函数的函数体的语句在执行之前会先调用B的构造函数,这时候问题在于,如果B的构造函数抛出了异常,A该如何捕获呢?一个迂回的做法是在A中把B的实例声明为指针,在构造函数和析构函数中分别创建和删除,这样就能捕获到异常了。不过,实际上是有更简单的做法的。下面我要介绍一个C++的很不常见的语法:函数作用域级别的异常捕获。
class B {
public:
B(int val) : val_(val * val) {
throw runtime_error("wtf from B");
}
private:
int val_;
};
class A {
public:
A(int val) try : b_(val) {
a_ = val;
} catch (runtime_error& e) {
cerr << e.what() << endl;
throw runtime_error("wtf from A");
}
private:
int a_;
B b_;
};
注意上面A的构造函数,在参数列表后和初始化列表前增加了try关键字,然后构造函数就被分割为了两部分,前面是初始化,后面是初始化时的错误处理。需要指出的是,catch块里面捕获到的异常不能被忽略,即catch块中必须有一个throw语句重新抛出异常,如果没有,则默认会将原来捕获到的异常重新抛出,这和一般的行为是不同的。例如下面代码运行可以发现A会将捕获到的异常原封不动抛出:
class A {
public:
A(int val) try : b_(val) {
a_ = val;
} catch (runtime_error& e) {
cerr << e.what() << endl;
}
private:
int a_;
B b_;
};
这种语法是C++的标准,而且目前已经被所有的主流C++编译器支持(VS2010、g++ 4.2、clang 3.1),所以几乎不存在兼容性问题,大可放心使用。
其他语言中的错误处理
Java倾向于大量使用异常,而且还把异常分为了两类分别是检查型异常(Checked Exception)和非检查型异常(Unchecked Exception),检查型异常就是java.lang.Exception的子类,用于报告需要检查的错误,也就是正常的业务逻辑,错误主要是由用户产生的,方便恢复或给出提示,譬如打开不存在的文件。而非检查型异常则是真正的系统异常,通常由软件缺陷导致,如数组下标越界、错误的类型转换等,这类异常继承于java.lang.RuntimeException或java.lang.Error。
Python和Java一样也倾向于使用异常,并不一定真的发生故障才抛出异常,譬如字符串转换为整数,如果字符串不合法,Python会抛出一个ValueError异常。甚至Python的迭代器在调用next()时没有更多的结果时会抛出StopIteration异常。这是典型的用异常来处理正常控制流的方法,在Python中被广泛使用。按照优秀C++代码的标准来看,这是典型的对异常的滥用,既复杂又有额外开销,不推荐使用,但在Python中这是一个广泛遵循的约定。
相较于Java和Python,Go的错误处理是另一个极端,Go语言则根本没有异常的概念,而是普遍采用返回值的方式来表示错误,同时还提供了panic和recover语法。由于Go有多返回值的特性,避免了错误代码占用返回结果的弊端,所以你可以经常看到函数的最后一个返回值是error类型。由于总是用返回值传递错误,你可以看到Go代码中耦合了大量的错误处理,几乎再每条函数调用语句之后都有一个判断错误是否发生的语句。panic和recover机制十分类似于异常,程序在遇到panic时会一层一层退出调用栈,直到遇到recover。不过recover只在defer中定义,相当于一个函数只有一个recover,而且被recover恢复后会回到错误发生处继续向下执行代码。Go语言倾向于把一般错误都作为返回值传递,除非是非常可怕的、除了重置状态几乎无法恢复错误才会被panic语句抛出。
Go语言的recover机制和异常比起来,反倒更像Visual Basic语言中的On及
Error GoTo labelResume语法。这是一种非结构化的错误处理方式,具体是当声明有On的函数发生错误以后,会调转到对应的行号,如果再遇到了
Error GoTo labelResume语句就会返回发生错误的语句后面的一条继续执行,例如下面这段代码:
Sub ErrorDemo
On Error GoTo ErrorHandler
Dim a as Integer
a = 1/0 ' An error occurs.
Print a ' Go back here
Exit Sub
ErrorHandler:
' Code that handles errors.
Resume
End Sub
Visual Basic中还有On Error Resume Next这样的万能错误处理语句,即遇到错误以后直接忽略并继续执行,这是一种非常危险而且不负责任的做法,但却可以在早期的Visual
Basic代码中到处看到。事实上用返回值传递错误代码的时候许多人也并不处理而是直接忽略,这跟On Error Resume Next本质上没有什么区别,却比On危害更大——因为
Error Resume NextOn Error Resume Next至少还有个标记说明「老子就是这么不负责任」,但忽略错误返回值就难以被一眼发现了。
如何处理C++构造函数中的错误——兼谈不同语言的错误处理的更多相关文章
- 在WPF程序中使用摄像头兼谈如何使用AForge.NET控件(转)
前言: AForge.NET 是用C#写的一个关于计算机视觉和人工智能领域的框架,它包括图像处理.神经网络.遗传算法和机器学习等.在C#程序中使用摄像头,我习惯性使用AForge.NET提供的类库.本 ...
- 垃圾回收机制GC知识再总结兼谈如何用好GC(转)
作者:Jeff Wong 出处:http://jeffwongishandsome.cnblogs.com/ 本文版权归作者和博客园共有,欢迎围观转载.转载时请您务必在文章明显位置给出原文链接,谢谢您 ...
- zw版·Halcon与delphi(兼谈opencv)
zw版·Halcon与delphi(兼谈opencv) QQ群 247994767(delphi与halcon) <Halcon与delphi>系列,早两年就想写,不过一方面,因为Halc ...
- 创建 PDO 实例并在构造函数中设置错误模式
PDO 将只简单地设置错误码,可使用 PDO::errorCode() 和 PDO::errorInfo() 方法来检查语句和数据库对象.如果错误是由于对语句对象的调用而产生的,那么可以调用那个对象的 ...
- [改善Java代码]不要在构造函数中抛出异常
Java的异常机制有三种: 一.Error类以及其子类表示的是错误,它是不需要程序员处理也不能处理的异常.比如VirtualMachineError虚拟机错误,ThreadDeath线程僵尸等. 二. ...
- [转] Portable Trac 简单介绍 - 兼谈为什么不选择 Redmine
Portable Trac 简单介绍 - 兼谈为什么不选择 Redmine Trac是一个轻量级的软件项目管理环境,如果在工作中涉及一个开发团队的管理并且关心项目管理工具的话,相信都在 Trac. ...
- 解决在构造函数中使用Session,Session为null的问题
问题描述: public abstract class PageBase : System.Web.UI.Page 在PageBase中如何使用Session??? 我直接用 Session[&quo ...
- 探索js原型链和vue构造函数中的奥妙
这篇文章首先会讲到原型链以及原型链的一些概念,然后会通过分析vue的源码,来看一下vue的构造函数是如何被创建的,now we go! 一.什么是原型链? 简单回顾下构造函数,原型和实例的关系: ...
- TCP的状态兼谈Close_Wait和Time_Wait的状态
原文链接: http://www.2cto.com/net/201208/147485.html TCP的状态兼谈Close_Wait和Time_Wait的状态 一 TCP的状态: 1).LIST ...
随机推荐
- Spark记录-spark介绍
Apache Spark是一个集群计算设计的快速计算.它是建立在Hadoop MapReduce之上,它扩展了 MapReduce 模式,有效地使用更多类型的计算,其中包括交互式查询和流处理.这是一个 ...
- Linux下/etc/passwd、/etc/shadow、/etc/group文件
1./etc/passwd [root@prac ~]# cat /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbi ...
- C#的Lamda表达式_匿名函数
- js截取图片上传(仅原理)----闲的无聊了代码就不共享了!写的难看,不好意思给你们看了(囧)
就算世界再坑爹,总有一些属性能带你走出绝望(伟大的absolute) 今天吐槽一下!......在我的世界里没有正统UI,所以效果图永远都是那么坑爹! 这里我要感谢有个position:absolut ...
- AngularJS入门基础——表单验证
<form name="form" novalidata> <label name="email">your email</l ...
- linux - 流量切分线路
流量切分线路方式 # 程序判断进入IP线路,设置服务器路由规则控制返回 vi /etc/iproute2/rt_tables #添加一条策略 bgp2 #注意策略的序号顺序 ip route add ...
- 矩阵乘法优化DP
本文讲一下一些基本的矩阵优化DP的方法技巧. 定义三个矩阵A,B,C,其中行和列分别为$m\times n,n \times p,m\times p$,(其中行是从上往下数的,列是从左往右数的) $C ...
- spm
Spatial Pyramid Matching 看了很多关于SPM的介绍,但是网络上的资源大多都是对论文Beyond bags of features: Spatial pyramid matchi ...
- requests(三):json请求中中文乱码处理
最近收到一个问题:json格式请求数据中有中文,导致服务端签名失败. 问题详情: 一位同学在发送json格式的post请求时,请求数据中有中文内容: {"inputCodes":[ ...
- ecshop 2.7.x 批量测试
下面为测试是否存在漏洞的脚本: sub MAIN($url) { use HTTP::UserAgent; my $r = HTTP::Request.new(); $r.uri: $url~'/us ...