Fru*_*nsi 308 c++ exception-handling
异常处理(EH)似乎是当前的标准,并且通过搜索网络,我找不到任何试图改进或替换它的新颖想法或方法(好吧,存在一些变化,但没有新颖的).
虽然大多数人似乎忽略它或只是接受它,但EH 有一些巨大的缺点:代码看不到异常,它会创建许多可能的退出点.乔尔在软件上写了一篇关于它的文章.比较goto完美,它让我再次思考EH.
我尽量避免使用EH,只使用返回值,回调或任何适合目的的东西.但是当你必须编写可靠的代码时,你现在就无法忽略EH:它从new可能抛出异常开始,而不是仅仅返回0(就像过去一样).这使得任何C++代码行都容易受到异常的影响.然后C++基础代码中的更多地方抛出异常...... std lib执行它,依此类推.
这感觉就像走在摇摇欲坠的地面上.所以,现在我们被迫关注异常!
但它很难,真的很难.你必须学会编写异常安全代码,即使你有一些经验,它仍然需要仔细检查任何一行代码是安全的!或者你开始在任何地方放置try/catch块,这会使代码混乱,直到它达到不可读状态.
EH取代了旧的干净确定性方法(返回值..),它只有一些但可以理解且易于解决的缺点,在您的代码中创建许多可能的退出点的方法,以及如果您开始编写捕获异常的代码(您是什么的)在某些时候被迫做某事),然后它甚至通过你的代码创建了许多路径(catch块中的代码,考虑一个服务器程序,你需要除了std :: cerr之外的日志工具..).EH有优势,但这不是重点.
我的实际问题:
pae*_*bal 514
你的问题断言,"编写异常安全的代码非常困难".我先回答你的问题,然后回答他们背后隐藏的问题.
你真的写异常安全代码吗?
我当然是了.
这就是Java作为C++程序员(缺乏RAII语义)对我失去了很多吸引力的原因,但我很离题:这是一个C++问题.
事实上,当您需要使用STL或Boost代码时,这是必要的.例如,C++线程(boost::thread或std::thread)将抛出异常以正常退出.
您确定最后一个"生产就绪"代码是异常安全的吗?
你能确定吗,它是吗?
编写异常安全的代码就像编写无错误的代码一样.
您不能100%确定您的代码是异常安全的.但是,然后,你使用众所周知的模式,并避免众所周知的反模式来争取它.
你知道和/或实际使用有效的替代品吗?
在C++ 中没有可行的替代方案(即你需要恢复到C并避免使用C++库,以及像Windows SEH这样的外部惊喜).
编写异常安全的代码,你必须知道首先每个你写的指令是什么级别的异常安全的.
例如,a 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
}
Run Code Online (Sandbox Code Playgroud)
我用这种分析编写了所有代码.
提供的最低保证是基本的,但是,每条指令的顺序使整个函数"无",因为如果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
}
Run Code Online (Sandbox Code Playgroud)
现在,我们的代码提供了"基本"保证.没有任何东西会泄漏,所有物体都将处于正确的状态.但我们可以提供更多,即强有力的保证.这是它可能变得昂贵的地方,这就是为什么并非所有 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
}
Run Code Online (Sandbox Code Playgroud)
我们重新订购了这些操作,首先创建并设置X其正确的价值.如果任何操作失败,则t不会被修改,因此,操作1到3可以被认为是"强":如果抛出某些东西,t则不会被修改,并且X不会泄漏,因为它由智能指针拥有.
然后,我们创建一个副本t2的t,如果事情上抛出这个副本从操作工作4至7,t2被修改,但之后,t仍然是原来的.我们仍然提供强有力的保证.
然后,我们交换t和t2.交换操作应该不是在C++中进行操作,所以让我们希望你所写的交换T是不可靠的(如果不是,则重写它以便它不是).
因此,如果我们到达函数的末尾,一切都成功(不需要返回类型)并t具有其例外值.如果失败,那么t仍然是其原始值.
现在,提供强有力的保证可能会非常昂贵,所以不要努力为您的所有代码提供强有力的保证,但如果您可以不花费成本(并且C++内联和其他优化可以使所有代码无法成本) ,然后去做.功能用户会感谢你.
编写异常安全的代码需要一些习惯.您需要评估您将使用的每条指令所提供的保证,然后,您需要评估指令列表提供的保证.
当然,C++编译器不会备份保证(在我的代码中,我提供了作为@warning doxygen标签的保证),这有点令人遗憾,但它不应该阻止您尝试编写异常安全的代码.
程序员如何保证无故障功能永远成功?毕竟,该功能可能有一个错误.
这是真的.异常保证应该由无错误的代码提供.但是,在任何语言中,调用函数都假定函数没有错误.没有合理的代码可以保护自己不受错误的影响.尽可能地编写代码,然后提供保证,假设它没有错误.如果有错误,请更正错误.
例外情况是异常处理失败,而不是代码错误.
现在,问题是"这值得吗?".
当然如此.知道函数不会失败的"nothrow/no-fail"函数是一个很大的好处.对于"强"函数也是如此,它使您能够使用事务语义编写代码,如数据库,具有提交/回滚功能,提交是代码的正常执行,抛出异常是回滚.
那么,"基本"是您应该提供的最低保证.C++是一种非常强大的语言,具有其范围,使您能够避免任何资源泄漏(垃圾收集器会发现很难为数据库,连接或文件句柄提供).
所以,据我所知,这是值得的.
nobar做了一个我相信的评论是非常相关的,因为它是"你如何编写异常安全代码"的一部分:
swap()函数的一个很好的建议.但应注意,std::swap()根据内部使用的操作,可能会失败默认情况下std::swap将进行复制和分配,对于某些对象,可以进行复制和分配.因此,默认交换可以抛出,既可用于您的类,也可用于STL类.就C++标准而言,交换操作是vector,deque和list不会抛出,而它可以用于map比较仿函数可以抛出复制结构(参见C++编程语言,特别版,附录E,E.4.3) .Swap).
查看向量交换的Visual C++ 2008实现,如果两个向量具有相同的分配器(即正常情况),则向量的交换将不会抛出,但如果它们具有不同的分配器,则将进行复制.因此,我认为它可以抛弃最后一种情况.
所以,原始文本仍然存在:不要写一个抛出交换,但必须记住nobar的注释:确保你交换的对象有一个非投掷交换.
Dave Abrahams给了我们基本/强/非保证,在一篇文章中描述了他使STL异常安全的经验:
http://www.boost.org/community/exception_safety.html
请看第7点(异常安全的自动化测试),他依靠自动化单元测试来确保每个案例都经过测试.我想这部分是对作者的问题的一个很好的答案:" 你能确定,它是什么? ".
t.integer += 1;是不能保证不会发生溢出而不是异常安全,事实上可能在技术上调用UB!(有符号溢出是UB:C++ 11 5/4"如果在求值表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义.")注意无符号整数不会溢出,而是在模数为2 ^#位的等价类中进行计算.
Dionadar指的是以下行,它确实具有未定义的行为.
t.integer += 1 ; // 1. nothrow/nofail
Run Code Online (Sandbox Code Playgroud)
这里的解决方案是std::numeric_limits<T>::max()在执行添加之前验证整数是否已经处于其最大值(使用).
我的错误将出现在"正常失败与错误"部分,即一个错误.它不会使推理无效,并不意味着异常安全的代码是无用的,因为无法实现.您无法保护自己免受计算机关闭,编译器错误,甚至您的错误或其他错误.你无法达到完美,但你可以尝试尽可能接近.
我用Dionadar的评论更正了代码.
D.S*_*ley 17
首先(正如Neil所说),SEH是微软的结构化异常处理.它与C++中的异常处理类似但不完全相同.实际上,如果在Visual Studio中需要它,则必须启用C++异常处理 - 默认行为不保证在所有情况下都会销毁本地对象!在任何一种情况下,异常处理并不是真的更难,它只是不同.
现在为您提出实际问题.
你真的写异常安全代码吗?
是.在所有情况下,我都努力寻求异常安全代码.我使用RAII技术宣传资源的范围访问(例如,boost::shared_ptr用于内存,boost::lock_guard用于锁定).通常,RAII和范围保护技术的一致使用将使异常安全代码更容易编写.诀窍是了解存在的内容以及如何应用它.
您确定最后一个"生产就绪"代码是异常安全的吗?
不,这是安全的.我可以说,由于几年的24/7活动中的异常,我没有看到过程故障.我不希望完美的代码,只是编写良好的代码.除了提供异常安全性之外,上述技术还以几乎不可能用try/ catch块实现的方式保证了正确性.如果您正在捕获最高控制范围(线程,进程等)中的所有内容,那么您可以确保在异常(大多数情况下)中继续运行.同样的技术也将帮助您继续运行正确的异常的脸而不try/ catch块随处可见.
你能确定它是吗?
是.您可以通过彻底的代码审核来确定,但没有人真正做到这一点吗?定期的代码审查和细心的开发人员在很长的路要走.
你知道和/或实际使用有效的替代品吗?
多年来我尝试了一些变化,例如在高位(ala HRESULTs)中编码状态或那种可怕的setjmp() ... longjmp()黑客.这两种方式在实践中都以完全不同的方式分解.
最后,如果您习惯于应用一些技巧并仔细考虑在异常情况下实际可以执行某些操作的位置,那么最终会得到非常易读且异常安全的代码.您可以按照以下规则进行总结:
try/ catch何时可以对特定异常执行某些操作new或delete代码std::sprintf,snprintf和一般的阵列-使用std::ostringstream的格式,代之以阵列std::vector和std::string我只能建议您学习如何正确使用异常,如果计划用C++编写,请忘记结果代码.如果您想避免异常,您可能需要考虑用另一种语言编写,或者使用它们,或者使它们安全.如果你想真正学习如何充分利用C++,请阅读Herb Sutter,Nicolai Josuttis和Scott Meyers的一些书.
在"任何行可以抛出"的假设下,不可能编写异常安全的代码.异常安全代码的设计主要依赖于您应该在代码中期望,观察,遵循和实现的某些合同/保证.保证永不抛出的代码是绝对必要的.还有其他种类的例外保证.
换句话说,创建异常安全代码在很大程度上是程序设计的问题,而不仅仅是普通编码问题.
小智 8
好吧,我当然打算.
我确信使用异常构建的24/7服务器可以全天候运行并且不会泄漏内存.
很难确定任何代码是否正确.通常,人们只能按结果去做
没有.使用例外比我在编程过去30年中使用的任何替代方案更清晰,更容易.
抛开SEH和C++异常之间的混淆,您需要意识到可以随时抛出异常,并在编写代码时考虑到这一点.对异常安全的需求在很大程度上推动了RAII,智能指针和其他现代C++技术的使用.
如果你遵循完善的模式,编写异常安全的代码并不是特别困难,事实上它比编写在所有情况下都能正确处理错误返回的代码更容易.