我们最近遇到了将C++框架移植到运行uClinux的ARM平台的问题,其中唯一受供应商支持的编译器是GCC 2.95.3.我们遇到的问题是,异常非常不可靠,导致一切从未被捕获到被不相关的线程(!)捕获.这似乎是一个记录在案的错误,即此处和此处.
经过一番考虑后,我们决定消除异常,因为我们已经达到了异常会对正在运行的应用程序造成大量破坏的程度.现在主要关注的是如何管理构造函数失败的情况.
我们尝试了懒惰的评估,其中每个方法都能够实例化动态资源并返回状态值,但这意味着每个类方法都必须返回一个返回值,这会在代码中产生很多 ifs并且在方法中非常烦人这通常不会导致错误.
我们研究了添加静态create方法,该方法返回指向创建对象的指针,如果创建失败则返回NULL,但这意味着我们不能再将对象存储在堆栈中,如果需要,仍需要传入对状态值的引用对实际错误采取行动.
根据谷歌的C++风格指南,他们不使用异常,只在他们的构造函数中做一些简单的工作,使用init方法进行非平凡的工作(在构造函数中做工作).但是,在使用这种方法时,我无法找到有关它们如何处理构造错误的信息.
有没有人在这里试图消除异常并提出一个很好的解决方案来处理施工失败?
通常,对于堆栈中的对象,最终会得到这样的代码:
MyClassWithNoThrowConstructor foo; if (foo.init(bar, baz, etc) != 0) { // error-handling code } else { // phew, we got away with it. Now for the next object... }
这适用于堆上的对象.我假设您使用返回NULL而不是抛出的东西覆盖全局运算符new,以节省自己记住在任何地方使用nothrow new:
MyClassWithNoThrowConstructor *foo = new MyClassWithNoThrowConstructor(); if (foo == NULL) { // out of memory handling code } else if (foo->init(bar, baz, etc) != 0) { delete foo; // error-handling code } else { // success, we can use foo }
显然,如果你可以,使用智能指针来节省必须记住删除,但如果你的编译器不能正确支持异常,那么你可能无法获得Boost或TR1.我不知道.
您也可能希望以不同方式构造逻辑,或者抽象组合的new和init,以避免在处理多个对象时深层嵌套的"箭头代码",并在两个故障情况之间共同处理错误.以上只是最辛苦形式的基本逻辑.
在这两种情况下,构造函数都将所有内容设置为默认值(它可以采用一些参数,前提是它对这些参数的作用不可能失败,例如,如果它只存储它们).然后,init方法可以执行可能失败的实际工作,并且在这种情况下返回0成功或任何其他失败值.
您可能需要强制执行整个代码库中的每个init方法以相同的方式报告错误:您不希望某些返回0成功或负错误代码,一些返回0成功或一个肯定的错误代码,一些返回bool,一些返回按值的对象,具有解释故障的字段,一些设置全局错误等.
您也许可以在线快速浏览一些Symbian类API文档.Symbian使用C++而没有例外:它确实有一个名为"Leave"的机制,它部分地弥补了这一点,但是从构造函数中保留是无效的,所以在设计非失败构造函数和推迟失败方面你有相同的基本问题对init例程的操作.当然使用Symbian时,init例程被允许保留,因此调用者不需要我在上面指出的错误处理代码,但是就C++构造函数和附加init调用之间的拆分工作而言,它是相同的.
一般原则包括:
如果构造函数想要以某种可能失败的方式从某个地方获取值,请将其推迟到init并将值默认初始化为ctor.
如果对象持有指针,请在ctor中将其设置为null,并在init中"正确"设置它.
如果您的对象包含引用,则将其更改为(智能)指针,以便它可以为null开始,或者使调用者将值作为参数传递给构造函数,而不是在ctor中生成它.
如果您的构造函数具有对象类型的成员,那么您没问题.他们的ctors也不会抛出,所以以通常的方式在初始化列表中构造你的成员(和基类)是完全可以的.
确保跟踪设置的内容和不设置的内容,以便析构函数在init失败时工作.
除了构造函数,析构函数和init之外的所有函数都可以假定init已成功,前提是您的类文档在调用init并成功之前调用除init之外的任何方法无效.
您可以提供多个init函数,这些函数与构造函数可以相互调用,就像您为某些类提供多个构造函数一样.
您无法提供可能失败的隐式转换,因此如果您的代码当前依赖于抛出异常的隐式转换,那么您必须重新设计.大多数操作符重载也是如此,因为它们的返回类型受到约束.