您的问题断言“编写异常安全代码非常困难”。我将首先回答您的问题,然后回答它们背后的隐藏问题。
回答问题
您是否真的编写异常安全代码?
我当然是了。
这就是Java作为C ++程序员对我失去吸引力的原因(缺乏RAII语义),但是我离题了:这是一个C ++问题。
实际上,当您需要使用STL或Boost代码时,这是必需的。例如,C ++线程(boost::thread
或std::thread
)将抛出异常以正常退出。
您确定最后一个“生产就绪”代码是异常安全的吗?
您甚至可以确定是吗?
编写异常安全代码就像编写无错误代码。
您不能100%确定您的代码是异常安全的。但是,随后您将使用知名的模式并避免使用知名的反模式,为此而努力。
您知道和/或实际使用可行的替代方法吗?
C ++ 中没有可行的替代方法(即,您需要还原到C并避免使用C ++库,以及Windows SEH之类的外部惊喜)。
编写异常安全代码
编写异常安全的代码,你必须知道首先每个你写的指令是什么级别的异常安全的。
例如,a new
可以引发异常,但是分配内置函数(例如int或指针)不会失败。交换永远不会失败(永远不要写std::list::push_back
引发交换),可以抛出...
例外保证
首先要了解的是,您必须能够评估所有功能提供的异常保证:
- none:您的代码永远都不能提供该代码。此代码将泄漏所有内容,并在引发第一个异常时分解。
- basic:这是您至少必须提供的保证,即,如果引发异常,则不会泄漏任何资源,并且所有对象仍然完整
- strong:处理将成功或引发异常,但是如果引发异常,则数据将处于完全未开始处理的状态(这使C ++具有事务处理能力)
- nothrow / nofail:处理将成功。
代码示例
以下代码似乎是正确的C ++,但实际上提供了“无”保证,因此,它是不正确的:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
我在编写所有代码时都会考虑到这种分析。
提供的最低保证是基本的,但是每个指令的排序使整个函数“无”,因为如果3.抛出,x将会泄漏。
首先要做的是使函数“基本”,即将x放入智能指针中,直到它被列表安全地拥有:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
现在,我们的代码提供了“基本”保证。什么都不会泄漏,所有对象都将处于正确的状态。但是我们可以提供更多,也就是有力的保证。这就是它可能变得昂贵的原因,这就是为什么并非所有 C ++代码都强大的原因。让我们尝试一下:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
我们对操作进行了重新排序,首先创建并设置X
为正确的值。如果任何操作失败,则t
不会被修改,因此,操作1到3可以被认为是“强”的:如果抛出的东西,t
未被修改并且X
不会泄漏,因为它归智能指针所有。
然后,我们创建一个副本t2
中t
,并从操作4这个副本的工作7.如果抛出的东西,t2
被修改,但之后,t
仍然是原来的。我们仍然提供有力的保证。
然后,我们交换t
和t2
。交换操作应在C ++中不被保留,因此我们希望您编写的交换T
内容不被保留(如果不是,则将其重写为不被保留)。
因此,如果我们到达函数的末尾,则一切都成功了(不需要返回类型),并且t
具有其例外值。如果失败,则t
仍具有其原始值。
现在,提供强有力的保证可能会非常昂贵,因此不要努力为您的所有代码提供强有力的保证,但是如果您可以无偿地做到这一点(C ++内联和其他优化可能会使上面的所有代码变得无成本) ,然后执行。函数用户将感谢您。
结论
编写异常安全代码需要一些习惯。您需要评估将要使用的每个指令所提供的保证,然后,您需要评估由指令列表所提供的保证。
当然,C ++编译器不会备份保证(在我的代码中,我将保证作为@warning doxygen标记提供),这有点令人遗憾,但是它不应阻止您尝试编写异常安全的代码。
正常故障与错误
程序员如何保证无故障功能将始终成功?毕竟,该功能可能存在错误。
这是真的。异常保证应该由无错误的代码提供。但是,无论哪种语言,调用函数都假定该函数没有错误。没有理智的代码可以防止出现错误。尽力编写代码,然后假设它没有错误,从而提供保证。如果有错误,请更正。
异常是由于异常的处理失败,而不是代码错误。
最后的话
现在,问题是“这值得吗?”。
当然是。具有“无/无失败”功能的人知道该功能不会失败是一个很大的福音。对于“强”功能也可以这样说,它使您能够使用事务语义来编写代码,例如具有提交/回滚功能的数据库,提交是代码的正常执行,抛出异常是回滚。
然后,“基本”是您应该提供的最起码的保证。C ++是一种非常强大的语言,它具有范围,可以避免任何资源泄漏(垃圾收集器会发现很难为数据库,连接或文件句柄提供某种东西)。
因此,据我所知,这是值得的。
编辑2010-01-29:关于非抛出交换
我相信nobar发表了一个评论,该评论非常相关,因为它是“如何编写异常安全代码”的一部分:
- [我]交换将永远不会失败(甚至不写引发交换)
- [nobar]对于自定义编写的
swap()
功能,这是一个很好的建议。但是,应注意的是,它可能会因其std::swap()
内部使用的操作而失败
默认情况下std::swap
将制作副本和分配,对于某些对象,这些副本和分配可以抛出。因此,默认的交换可能会抛出,或者用于您的类,甚至用于STL类。至于C ++标准而言,对于交换操作vector
,deque
及list
不会抛出,而它可以用于map
如果比较仿函数可以拷贝构造(见抛出C ++编程语言,特别版,附录E,E.4.3交换)。
查看向量交换的Visual C ++ 2008实现,如果两个向量具有相同的分配器(即正常情况),则向量的交换将不会抛出,但是如果它们具有不同的分配器,则会进行复制。因此,我认为它可能会在最后一种情况下抛出。
因此,原始文本仍然有效:永远不要编写引发抛出的交换,但是必须记住nobar的注释:确保要交换的对象具有非抛出交换。
编辑2011-11-06:有趣的文章
给我们提供基本/强/无担保的Dave Abrahams在一篇文章中描述了他使STL异常安全的经验:
http://www.boost.org/community/exception_safety.html
看一下第七点(异常安全的自动测试),他依靠自动单元测试来确保对每个案例都进行了测试。我想这部分是问题作者“ 您什至可以确定,是吗? ” 的绝佳答案。
t.integer += 1;
不能保证不会发生溢出,也不是异常安全的,实际上可以从技术上调用UB!(带符号的溢出是UB:C ++ 11 5/4“如果在对表达式求值时,结果在数学上未定义或不在其类型的可表示值范围内,则行为是不确定的。”)请注意,未签名整数不会溢出,但会以等价类以2 ^#bits为模进行计算。
Dionadar指的是以下行,该行确实具有未定义的行为。
t.integer += 1 ; // 1. nothrow/nofail
解决方案是std::numeric_limits<T>::max()
在进行加法之前,验证整数是否已经达到最大值(使用)。
我的错误将出现在“正常故障与错误”部分,即一个错误。它不会使推理无效,也并不意味着异常安全代码是无用的,因为无法实现。您无法保护自己免受计算机关闭,编译器错误,甚至错误或其他错误的影响。您无法达到完美,但是您可以尝试尽可能地接近。
我改正了Dionadar的注释后的代码。