你(真的)写出异常安全的代码吗?
异常处理(EH)似乎是当前的标准,并且通过搜索网络,我找不到任何尝试改进或取代它的新颖想法或方法(当然,存在一些变化,但没有新颖性)。
尽管大多数人似乎忽略了它,或者只是接受它,但EH 有一些巨大的缺点:代码中看不到异常,它创造了许多可能的退出点。 乔尔在软件上写了一篇关于它的文章。 与goto
的比较非常适合,这让我再次想到了EH。
我尽量避免使用EH,只使用返回值,回调或任何符合目的的东西。 但是当你必须编写可靠的代码时,你现在不能忽略EH :它以new
开始,它可能会抛出一个异常,而不是仅仅返回0(就像过去一样)。 这使得任何一行C ++代码都容易受到异常的影响。 然后,C ++基础代码中的更多地方会抛出异常...... std lib会执行此操作,依此类推。
这感觉就像走在摇摇欲坠的理由上 。所以,现在我们被迫关心异常!
但它很难,它真的很难。 你必须学会写异常安全的代码,即使你有一些经验,仍然需要仔细检查任何一行代码才能安全! 或者你开始在任何地方放置try / catch块,这会混淆代码,直到达到不可读状态。
EH取代了旧的清晰的确定性方法(返回值..),它只有一些但易于理解和易于解决的缺点,并在代码中创建了许多可能的退出点,并且如果您开始编写捕获异常的代码(您在某些时候被迫做),然后它甚至通过你的代码创建了许多路径(在catch块中的代码,想想你需要日志工具而不是std :: cerr的服务器程序..)。 EH有优势,但这不是重点。
我的实际问题:
你的问题提出了一个断言,即“编写异常安全代码非常困难”。 我会先回答你的问题,然后回答隐藏在他们身后的问题。
回答问题
你真的写出异常安全的代码吗?
我当然是了。
这就是Java作为一名C ++程序员(缺乏RAII语义)失去了很多吸引我的原因,但我离题了:这是一个C ++问题。
当您需要使用STL或Boost代码时,它实际上是必需的。 例如,C ++线程( boost::thread
或std::thread
)将抛出一个异常以优雅地退出。
你确定你最后的“生产就绪”代码是异常安全的吗?
你甚至可以肯定,这是吗?
编写异常安全的代码就像编写无错代码一样。
你不能100%确定你的代码是异常安全的。 但是,那么你就会努力使用众所周知的模式,并避免众所周知的反模式。
你知道和/或实际使用替代品吗?
在C ++中没有可行的替代方案(即,您需要恢复到C,并避免使用C ++库,以及Windows SEH等外部意外)。
编写异常安全代码
编写异常安全的代码,您必须首先了解每个你写的指令是什么级别的异常安全的。
例如,一个new
可以抛出异常,但分配一个内置的(例如一个int或一个指针)不会失败。 一个交换将永远不会失败(不要写一个抛出交换),一个std::list::push_back
可以抛出...
例外保证
首先要了解的是,您必须能够评估所有功能提供的异常保证:
代码示例
下面的代码看起来像是正确的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
。 Swap操作应该不会在C ++中出现,所以我们希望你为T
写的交换不是空的(如果不是,重写它,这样它就不会出现)。
所以,如果我们到达函数的末尾,一切都成功了(不需要返回类型)并且t
有它的例外值。 如果失败了,那么t
仍然是它的原始价值。
现在,提供强有力的保证可能会非常昂贵,所以不要努力为您的所有代码提供强有力的保证,但是如果您可以在没有成本的情况下做到这一点(并且C ++内联和其他优化可以使所有代码成本高于成本) ,然后做。 功能用户会感谢你。
结论
编写异常安全的代码需要一些习惯。 您需要评估您将使用的每条说明所提供的保证,然后,您需要评估一系列说明所提供的保证。
当然,C ++编译器不会提供保证(在我的代码中,我提供了一个@warning doxygen标签),这有点令人伤心,但它不应该阻止您尝试编写异常安全的代码。
正常失败与错误
程序员如何保证nofail函数总能成功? 毕竟,该功能可能有一个错误。
这是真的。 异常保证应该由无错代码提供。 但是,在任何语言中,调用一个函数都假设该函数没有bug。 没有理智的代码可以保护自己免受bug的可能性。 编写最好的代码,然后,提供保证,假设它没有bug。 如果有错误,纠正它。
例外情况是出现异常处理失败,而不是代码错误。
最后的话
现在,问题是“这值得吗?”。
当然如此。 知道函数不会失效的“nothrow / nofail”函数是一个很大的好处。 对于“强”功能也是如此,它使您能够使用事务性语义编写代码,如数据库,具有提交/回滚功能,提交正常执行代码,引发异常是回滚。
那么,“基本”是你应该提供的最低保证。 C ++是一种非常强大的语言,它的范围可以避免任何资源泄漏(垃圾收集器会发现数据库,连接或文件句柄难以提供)。
所以,据我看来,这是值得的。
编辑2010-01-29:关于非投掷交换
nobar发表了一篇评论,我认为这很有意义,因为它是“你如何编写异常安全代码”的一部分:
swap()
函数的一个很好的建议。 但是,应该注意的是, std::swap()
可能会因内部使用的操作而失败 默认的std::swap
会进行复制和分配,这对于某些对象可能会抛出。 因此,默认的交换可以抛出,用于你的类,甚至用于STL类。 就C ++标准而言, vector
, deque
和list
的交换操作不会抛出,而如果比较函子可以抛出复制构造,它可以用于map
(参见C ++编程语言特别版附录E ,E.4.3.Swap)。
看看向量交换的Visual C ++ 2008实现,如果两个向量具有相同的分配器(即正常情况),则向量的交换不会抛出,但如果它们具有不同的分配器,则会进行复制。 因此,我认为它可以抛出最后一种情况。
因此,原始文本仍然存在:不要写一个丢弃交换,但必须记住nobar的注释:确保您交换的对象具有非抛出交换。
编辑2011-11-06:有趣的文章
Dave Abrahams给了我们一些基本的/强大的/不值得保证的信息,他在一篇文章中描述了他关于STL异常安全的经验:
http://www.boost.org/community/exception_safety.html
看看第7点(自动化异常安全测试),他依靠自动化单元测试来确保每个案例都经过测试。 我想这个部分对问题作者的“你甚至可以肯定,它是?”是一个很好的答案。
编辑2013-05-31:来自dionadar的评论
t.integer += 1;
没有保证溢出不会发生异常安全,事实上可以在技术上调用UB! (签名溢出是UB:C ++ 11 5/4“如果在评估表达式时,结果不是数学定义的或者不在其类型的可表示值范围内,则行为是未定义的。”)注意,无符号整数不会溢出,但在等价类模2 ^#位中进行计算。
Dionadar提到了下面这一行,它确实有未定义的行为。
t.integer += 1 ; // 1. nothrow/nofail
这里的解决方案是在添加之前验证整数是否已经达到其最大值(使用std::numeric_limits<T>::max()
)。
我的错误会出现在“正常失败与错误”部分,也就是一个错误。 它不会使推理失效,并不意味着异常安全的代码是无用的,因为无法实现。 您无法保护自己免受计算机关闭,编译器错误,甚至是错误或其他错误的影响。 你不能达到完美,但你可以尝试尽可能接近。
我根据Dionadar的评论修正了代码。
用C ++编写异常安全的代码并不是要使用大量try {} catch {}块。 这是关于记录你的代码提供什么样的保证。
我建议阅读香草萨特的本周系列大师,特别是分期付款59,60和61。
总而言之,您可以提供三种异常安全级别:
就我个人而言,我很晚才发现这些文章,所以我的C ++代码绝对不是异常安全的。
我们有些人20多年来一直在使用例外。 例如PL / I有他们。 我认为他们是一种新的危险技术的前提是值得怀疑的。
链接地址: http://www.djcxy.com/p/13261.html上一篇: Do you (really) write exception safe code?
下一篇: Center a flexbox item in the container, if there is already an item to the left