|
| A Pragmatic Look at Exception Specifications |
![]() | Guarantee that functions will only throw listed exceptions (possibly none). |
![]() | Enable compiler optimizations based on the knowledge that only listed exceptions (possibly none) will be thrown. |
The above expectations are, again, deceptively close to being correct. Consider again the code in Example 1(b):
上述期望再次看似正确实则不然。请重新审视示例 1(b)中的代码:
// Example 1(b) reprise, and two
// 示例 1(b)重现,以及两个
// potential white lies: // 潜在的善意谎言:
//
int Gunc() throw(); // will throw nothing (?)
int Gunc() throw(); // 不会抛出任何异常?(真的吗?)
int Hunc() throw(A,B); // can only throw A or B (?)
int Hunc() throw(A,B); // 只能抛出 A 或 B?(确定吗?)
Are the comments correct? Not quite. Gunc() may indeed throw something, and Hunc() may well throw something other than A or B! The compiler just guarantees to beat them senseless if they do… oh, and to beat your program senseless too, most of the time.
注释准确吗?不尽然。Gunc()实际上可能抛出异常,而 Hunc()也完全可能抛出 A 或 B 之外的异常!编译器只是保证在它们违规时痛揍它们一顿……哦,顺便在大多数时候也会把你的程序揍得七荤八素。
Because Gunc() or Hunc() could indeed throw something they promised not to, not only can’t the compiler assume it won’t happen, but the compiler is responsible for being the policeman with the billy club who checks to make sure such a bad thing doesn’t happen undetected. If it does happen, then the compiler must invoke the unexpected() function. Most of the time, that will terminate your program. Why? Because there are only two ways out of unexpected(), neither of which is a normal return. You can pick your poison:
由于 Gunc()或 Hunc()确实可能抛出它们承诺不会抛出的异常,编译器不仅不能假设这种情况不会发生,还必须扮演手持警棍的警察角色,确保此类违规行为不会悄无声息地发生。一旦发生,编译器必须调用 unexpected()函数。大多数情况下,这会导致程序终止。为什么?因为 unexpected()只有两种非正常返回的退出途径,任选其一都是饮鸩止渴:
a) Throw instead an exception that the exception specification does allow. If so, the exception propagation continues as it would normally have. But remember that the unexpected() handler is global — there is only one for the whole program. A global handler is highly unlikely to be smart enough to Do the Right Thing for any given particular case, and the result is to go to terminate(), go directly to terminate(), do not pass catch, do not collect $200.
a) 改抛异常规范允许的异常。若如此,异常传播会按正常流程继续。但请记住 unexpected()处理器是全局的——整个程序只有一个。全局处理器极不可能针对每个具体场景做出明智处理,结果往往是直奔 terminate()而去,直接跳转到 terminate(),不经过 catch,也领不到 200 美元。b) Throw instead (or rethrow) an exception that the exception specification (still) doesn’t allow. If the original function allowed a bad_exception type in its exception specification, okay, then it’s a bad_exception that will now get propagated. But if not, then go to terminate(), go directly to terminate()…
b) 抛出(或重新抛出)异常规范仍不允许的异常。如果原函数在其异常规范中允许 bad_exception 类型,那么没问题,此时将传播一个 bad_exception。但如果不允许,就直接跳到 terminate(),直接跳到 terminate()……
Because violated exception specifications end up terminating your program the vast majority of the time, I think it’s legitimate to call that “beat[ing] your program senseless.”
由于违反异常规范绝大多数情况下会导致程序终止,我认为将其称为“把程序打得晕头转向”是合理的。
Above, we saw two bullets stating what many people think that exception specifications do. Here is an edited statement that more accurately portrays what they actually do do:
上文我们看到两个要点,陈述了许多人认为异常规范的作用。以下是经过编辑的陈述,更准确地描述了它们实际的作用:
![]() |
|
![]() | Enable or prevent compiler optimizations |
To see what a compiler has to do, consider the following code which provides a body for one of our sample functions, Hunc():
要理解编译器所需执行的操作,请看以下为我们的示例函数 Hunc()提供主体的代码:
// Example 3(a) // 示例 3(a)
//
int Hunc() throw(A,B) int Hunc() 抛出异常(A,B)
{
return Junc(); 返回 Junc();
}
Functionally, the compiler must generate code like the following, and it’s typically just as costly at runtime as if you’d hand-written it yourself (though less typing because the compiler generates it for you):
从功能上讲,编译器必须生成类似以下的代码,其运行时成本通常与你手动编写的一样高(尽管输入量更少,因为编译器为你生成了它):
// Example 3(b): A compiler’s massaged
// 示例 3(b):编译器对示例 3(a)的调整版本
// version of Example 3(a)
// 示例 3(a)的修改版
//
int Hunc()
try
{
return Junc(); 返回 Junc();
}
catch( A ) 捕获( A )
{
throw; 抛出;
}
catch( B ) 捕获( B )
{
throw; 抛出;
}
catch( ... ) 捕获( ... )
{
std::unexpected(); // will not return! but
std::unexpected(); // 不会返回!但是
} // might throw an A or a B if you’re lucky
} // 如果运气好,可能会抛出 A 或 B
Here we can see more clearly why, rather than letting the compiler make optimizations by assuming only certain exceptions will be thrown, it’s exactly the reverse: the compiler has to do more work to enforce at runtime that only those exceptions are indeed thrown.
这里我们可以更清楚地看到,与其让编译器通过假设只会抛出某些异常来进行优化,情况恰恰相反:编译器必须在运行时做更多工作来确保只有那些异常确实被抛出。
Besides the overhead for generating the try/catch blocks shown above, which might be minor on efficient compilers, there are at least two other ways that exception specifications can commonly cost you in runtime performance. First, some compilers will automatically refuse to inline a function having an exception specification, just as they can apply other heuristics such as refusing to inline functions that have more than a certain number of nested statements or that contain any kind of loop construct. Second, some compilers don’t optimize exception-related knowledge well at all, and will add the above-shown try/catch blocks even when the function body provably can’t throw.
除了生成上述 try/catch 块的开销(在高效编译器上可能较小)之外,异常规范通常还会以至少两种其他方式在运行时性能上造成损失。首先,一些编译器会自动拒绝内联具有异常规范的函数,就像它们可以应用其他启发式方法一样,例如拒绝内联具有超过一定数量嵌套语句或包含任何类型循环构造的函数。其次,一些编译器根本无法很好地优化与异常相关的知识,并且会在函数体明显无法抛出时仍然添加上述 try/catch 块。
Moving beyond runtime performance, exception specifications can cost you programmer time because they increase coupling. For example, removing a type from the base class virtual function’s exception specification is a quick and easy way to break lots of derived classes in one swell foop (if you’re looking for a way). Try it on a Friday afternoon checkin, and start a pool to guess the number of angry emails that will be waiting for you in your inbox on Monday morning.
抛开运行时性能不谈,异常规范会因增加耦合性而耗费程序员时间。例如,从基类虚函数的异常规范中移除某个类型,是快速破坏大量派生类的便捷方法(如果你想找这样的方法)。不妨在周五下午提交代码时试试,并开个赌局猜猜周一早上邮箱里会有多少愤怒的邮件等着你。
So here’s what seems to be the best advice we as a community have learned as of today:
因此,以下似乎是我们社区迄今为止总结出的最佳建议:
Moral #1: Never write an exception specification.
教训一:永远不要编写异常规范。Moral #2: Except possibly an empty one, but if I were you I’d avoid even that.
教训二:或许可以写个空的,但换作是我连这都不会写。
Boost’s experience is that a throws-nothing specification on a non-inline function is the only place where an exception specification “may have some benefit with some compilers” [emphasis mine]. That’s a rather underwhelming statement in its own right, but a useful consideration if you have to write portable code that will be used on more than one compiler platform.
Boost 的经验表明,在非内联函数上使用"不抛出任何异常"的规范,是"可能在某些编译器上带来些许好处"[原文强调]。这种说法本身已相当保守,但若你需要编写跨多编译器平台的便携代码,这仍是个值得考虑的细节。
It’s actually even a bit worse than that in practice, because it turns out that popular implementations vary in how they actually handle exception specifications. At least one popular C++ compiler (Microsoft’s, up to version 7.x) parses exception specifications but does not actually enforce them, reducing the exception specifications to glorified comments. But, on the other hand, there are legal optimizations a compiler can perform outside a function, and which the Microsoft 7.x compiler does perform, that rely on the ES enforcement being done inside each function -- the idea is that if the function did try to throw something it shouldn’t the internal handler would stop the program and control would never return to the caller, so since control did return to the caller the calling code can assume nothing was thrown and do things like eliminate external try/catch blocks. So on that compiler, because the checking is not done but the legal optimization that relies on it is done, the meaning of “throw()” changes from the standard “check me on this, stop me if I inadvertently throw” to a “trust me on this, assume I’ll never throw and optimize away.” So beware: If you do choose to use even an empty throw-specification, read your compiler’s documentation and check to see what it will really do with it. You might just be surprised. Be aware, drive with care.
实际上在实践中情况甚至更糟,因为事实证明,主流编译器对异常规范的处理方式各不相同。至少有一个流行的 C++编译器(微软的,直到 7.x 版本)会解析异常规范但实际上并不强制执行,使得异常规范沦为华而不实的注释。然而另一方面,编译器可以在函数外部进行合法的优化(微软 7.x 编译器确实这么做了),这些优化依赖于每个函数内部对异常规范的强制执行——其逻辑是,如果函数试图抛出不该抛出的异常,内部处理程序会终止程序,控制权永远不会返回给调用者,因此既然控制权回到了调用者,调用代码就可以假设没有异常抛出,从而进行诸如消除外部 try/catch 块等优化。所以在该编译器上,由于未执行检查却执行了依赖于此的合法优化,"throw()"的含义就从标准的"检查我是否违规抛出,若无意抛出则阻止"变成了"相信我绝不会抛出,请据此优化"。务必注意:即使你选择使用空异常规范,也要阅读编译器文档并确认其真实行为——你可能会大吃一惊。保持清醒,谨慎驾驶。
While mentioning this material as part of a broader talk at the ACCU conference this past spring, I asked how many of the about 100 people in the room each time had used exception specifications. About half put up their hands. Then a wag at the back said (quite correctly) that I should also ask how many of those people later took the exception specifications back out again afterwards, so I asked; about the same number of hands went up. This is telling. The world-class library designers at Boost went through the same experience, and that’s why their coding policy on writing exception specifications pretty much boils down to “don’t do that.” [2]
今年春天在 ACCU 大会的演讲中提及这个话题时,我现场询问约 100 名听众中有多少人使用过异常规范,约半数人举手。随后后排有位机灵鬼提议(非常正确)还应该问有多少人后来又删除了这些异常规范,当我追问时,举手人数几乎相同。这很能说明问题。Boost 库的世界级设计者们也有相同经历,因此他们的编码政策对异常规范的指导意见可归结为"别用这玩意儿"[2]。
True, many well-intentioned people wanted exception specifications in the language, and so that’s why we have them. This reminds me of a cute poem that I first encountered about 15 years ago as it circulated in midwinter holiday emails. Set to the cadence of “‘Twas the Night Before Christmas,” these days it’s variously titled “‘Twas the Night Before Implementation” or “‘Twas the Night Before Crisis.” It tells of a master programmer who slaves away late at night in the holiday season to meet user deadlines, and performs multiple miracles to pull out a functioning system that perfectly implements the requirements… only to experience a final metaphorical kick in the teeth as the last four lines of the ditty report:
诚然,许多出于善意的人们曾希望将异常规范引入语言,所以我们才有了它。这让我想起15年前在冬至节日邮件里流传的一首俏皮诗。这首诗仿照《圣诞前夜》的韵律,如今常被冠以《实现前夜》或《危机前夜》的标题。它讲述了一位大师程序员在节日季熬夜赶工满足用户需求,施展多重奇迹最终交付完美符合要求的系统……却在结尾遭遇隐喻性的沉重打击,正如诗的最后四行所述:
The system was finished, the tests were concluded,
系统已完成,测试皆通过,
The users’ last changes were even included.
用户最后的修改甚至被包含在内。
And the users exclaimed, with a snarl and a taunt,
用户们咆哮着,带着讥讽与嘲弄喊道,
“It’s just what we asked for, but not what we want!” [4]
“这正是我们所要求的,却非我们真正想要的!”[4]
The thought resonates as we finish considering our current experience with exception specifications. The feature seemed like a good idea at the time, and it is just what some asked for.
当我们反思当前对异常规范的使用体验时,这一想法引起了共鸣。该特性在当时看似是个好主意,也确实是一些人所要求的。
But wait, there’s more: Might the same be said about export? More on that one next time, when we return…
但且慢,还有更多:同样的情况是否也适用于导出功能?关于这一点,我们下次回来再谈……
[1] See www.edg.com.
[1] 参见 www.edg.com。
[2] Available via www.gotw.ca/publications/xc++s/boost_es.htm.
[2] 可通过 www.gotw.ca/publications/xc++s/boost_es.htm 获取。
[3] Herb Sutter. “Exception Safety and Exception Specifications – Are They Worth It?” (Guru of the Week #82).
[3] Herb Sutter. “异常安全与异常规范——它们值得吗?” ( 每周大师 #82).
[4] A web search for “a snarl and a taunt” will get you several variations on this poem. Enjoy! Alas, the original author of the poem is unknown (to me). If anyone has information about the original, or at least earliest known, source of this poem, please send me mail.
[4] 在网络上搜索“一声咆哮与一阵嘲弄”可以找到这首诗的多个版本。欣赏吧!遗憾的是,这首诗的原始作者已无从考证(至少对我来说)。如果有人知道这首诗最初或至少是最早的来源信息,请通过邮件联系我。
Copyright © 2009
Herb Sutter
|