这是用户在 2025-4-7 12:01 为 http://www.gotw.ca/publications/mill22.htm 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
A Pragmatic Look at Exception Specifications
异常规范的实际考量

Home Blog Talks Books & Articles Training & Consulting

Prev
Up
Next

On the  关于
blog  博客
RSS feed November 4: Other Concurrency Sessions at PDC
11 月 4 日: PDC 大会的其他并发议题

November 3
: PDC'09: Tutorial & Panel
11 月 3 日 PDC'09:教程与小组讨论
October 26: Hoare on Testing
10 月 26 日: 霍尔谈测试

October 23
: Deprecating export Considered for ISO C++0x
10 月 23 日 关于在 ISO C++0x 中考虑弃用 export 的讨论

A Pragmatic Look at Exception Specifications
异常规范的实际考量

This article appeared in C/C++ Users Journal, 20(7), July 2002.
本文发表于 《C/C++用户期刊》第 20 卷第 7 期,2002 年 7 月

 

As we consider work now underway on the new C++ standard, C++0x, it’s a good time to take stock of what we’re doing with, and have learned from, our experience with the current C++98 standard. The vast majority of Standard C++’s features are good, and they get the lion’s share of the print because there’s not much point harping on the weaker features. Rather, the weaker and less useful features more often just get ignored and atrophy from disuse until many people forget they’re even there (not always a bad thing). That’s why you’ve seen relatively few articles about obscure features like valarray, bitset, locales, and the legal expression 5[a] — and the same is true, for reasons which we shall see in this column and the next, for exception specifications and export.
当我们审视正在进行的新 C++标准 C++0x 工作时,正是时候总结我们从现行 C++98 标准中获得的经验教训。标准 C++的绝大多数特性都是优秀的,它们占据了主要篇幅,因为反复强调那些较弱特性的意义不大。相反,那些较弱且不太实用的特性往往被忽视,因长期不用而逐渐萎缩,直到许多人甚至忘记它们的存在(这未必是坏事)。这就是为什么你很少看到关于冷门特性如 valarray、bitset、locales 以及合法表达式 5[a]的文章——同样,正如我们将在本期和下一期专栏中看到的,异常规范和 export 关键字也面临类似处境。

This time, let’s take a closer look at the state of our experience with Standard C++ exception specifications.
这次,让我们更仔细地审视标准 C++异常规范的使用现状。

 

The Story So Far  迄今为止的故事

The idea behind exception specifications is easy to understand: In a C++ program, unless otherwise specified, any function might conceivably emit any type of exception. Consider a function named Func() (because the name f() is so dreadfully overused):
异常规范的初衷很容易理解:在 C++程序中,除非另有说明,否则任何函数都可能抛出任何类型的异常。考虑一个名为 Func()的函数(因为 f()这个名字已经被过度使用了):

// Example 1(a)  // 示例 1(a)
//
int Func();            // can throw anything
int Func(); // 可能抛出任何异常

By default, in C++, Func() could indeed throw anything, just as the comment says. Now, often we know just what kinds of things a function might throw, and then it’s certainly reasonable to want to supply the compiler and the human programmer with some information limiting what exceptions could come tearing out of a function. For example:
默认情况下,在 C++中,正如注释所言,Func()确实可能抛出任何异常。通常,我们仅知道函数可能抛出哪些类型的异常,因此自然希望为编译器和程序员提供一些信息,以限制函数可能抛出的异常类型。例如:

// Example 1(b)  // 示例 1(b)
//
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 类型异常

In these cases, the function’s exception specification exists in order to say something about what the functions Gunc() and Hunc() could emit. The comments document colloquially what the specifications say. We’ll return to that “colloquially” part in a moment, because as it turns out these two comments are deceptively close to being correct.
在这些情况下,函数的异常规范旨在说明 Gunc()和 Hunc()可能抛出的异常类型。注释以通俗的方式记录了这些规范的含义。我们稍后会回到这个“通俗”的部分,因为事实证明,这两条注释几乎正确,却暗藏微妙差异。

One might naturally think that making a statement about what the functions might throw would be a good thing, that more information is better. One would not necessarily be right, because the devil is in the details: Although the motivation is noble, the way exception specifications are, well, specified in C++ isn’t always useful and can often be downright detrimental.
人们很自然地会认为,明确声明函数可能抛出什么异常是件好事,毕竟信息越多越好。但这种想法未必正确,因为魔鬼藏在细节中:尽管初衷是好的,但 C++中异常规范的实现方式并不总是实用,甚至常常适得其反。

Issue the First: A “Shadow Type System”
首要问题:“影子类型系统”

John Spicer, of Edison Design Group fame [1] and an author of large swathes of the template chapter of the C++ standard, has been known to call C++’s exception specifications a “shadow type system.” One of C++’s strongest features is its strong type system, and that’s well and good. Why would we call exception specifications a “shadow type system” instead of just “part of the type system”?
以爱迪生设计集团闻名的 John Spicer[1]——他也是 C++标准模板章节大部分内容的作者——曾将 C++的异常规范称为“影子类型系统”。C++最强大的特性之一是其强类型系统,这固然很好。但为什么我们要把异常规范称作“影子类型系统”,而不是直接视为“类型系统的一部分”呢?

The reason is simple, and twofold:
原因很简单,且有两个方面:

a) Exception specifications don’t participate in a function’s type.
a) 异常规范不参与函数类型的构成。

b) Except when they do.
b) 除非它们确实如此。

Consider first an example of when exception specifications don’t participate in a function’s type. Reflect on the following code:
首先考虑一个异常规范不参与函数类型的例子。思考以下代码:

// Example 2(a): You can’t write an ES
// 示例 2(a):无法在 typedef 中编写异常规范

// in a typedef.  // 在 typedef 中。
//
void f() throw(A,B);

typedef void (*PF)() throw(A,B); // syntax error
typedef void (*PF)() throw(A,B); // 语法错误


PF pf = f;                       // can’t get here
PF pf = f;                       // 无法到达此处

The throw-specification on the typedef is illegal. C++ doesn’t let you write that, and so the exception specification is not allowed to participate in the type of a function… at least, not in the context of a typedef, it’s not. But in other cases, exception specifications do indeed participate in the function’s type, such as if you wrote the same function declaration without the typedef:
在 typedef 上指定的异常抛出是不合法的。C++不允许这样写,因此异常规范不允许参与函数的类型…至少在 typedef 的上下文中不允许。但在其他情况下,异常规范确实参与函数的类型,例如如果你不使用 typedef 写出相同的函数声明:

// Example 2(b): But you can if you omit
// 示例 2(b):但如果你省略

// the typedef!  // typedef 就可以!
//
void f() throw(A,B);
void (*pf)() throw(A,B);   // ok
void (*pf)() throw(A,B);   // 正确

pf = f;                    // ok
pf = f;                    // 正确

Incidentally, you can do this kind of assignment of a pointer to a function as long as the target’s exception specification is no more restrictive than the source’s:
顺便说一下,只要目标的异常规范不比源的更严格,你就可以进行这种函数指针的赋值:

// Example 2(c): Also kosher, low-carb,
// 示例 2(c):同样符合规范,低碳水化合物,

// and fat-free.  // 且无脂肪。
//
void f() throw(A,B);
void (*pf)() throw(A,B,C); // ok
void (*pf)() throw(A,B,C); // 正确

pf = f;                    // ok, less restrictive
pf = f;                    // 正确,限制更少

Exception specifications also participate in a virtual function’s type when you try to override it:
异常规范在尝试覆盖虚函数时也会参与其类型的确定:

// Example 2(d): And the ES in the signature
// 示例 2(d):签名中的异常规范

// does matter if it’s a virtual function.
// 若为虚函数则确实重要。

//
class C
{
  virtual void f() throw(A,B); // same ES
virtual void f() throw(A,B); // 相同的异常规范

};

class D : C
{
  void f(); // error, now the ES matters
void f(); // 错误,现在异常规范(ES)变得重要了

};

So the first issue with exception specifications as they exist in today’s C++ is that they’re really a “shadow type system” that plays by different rules than the rest of the type system.
因此,当今 C++中异常规范存在的首要问题是,它们实际上是一个“影子类型系统”,遵循的规则与类型系统的其余部分不同。

Issue the Second: (Mis)understandings
第二个问题:(误)理解

The second issue has to do with knowing what you’re getting. As many notable persons, including the authors of the Boost exception specification rationale [2], have put it, programmers tend to use exception specifications as though they behaved the way the programmer would like, instead of the way they actually do behave. (For a brief mention of this, with longer related discussion about whether exception safety is worth it, see [3].)
第二个问题涉及理解你所得到的内容。正如包括 Boost 异常规范基本原理[2]的作者在内的许多知名人士所言,程序员倾向于按照他们希望的方式使用异常规范,而不是按照它们实际的行为方式。(关于此点的简要提及及关于异常安全性是否值得的更长时间讨论,请参见[3]。)

Here’s what many people think that exception specifications do:
许多人认为异常规范的作用如下:

bullet

Guarantee that functions will only throw listed exceptions (possibly none).
确保函数仅抛出列出的异常(可能没有)。

bullet

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:
上文我们看到两个要点,陈述了许多人认为异常规范的作用。以下是经过编辑的陈述,更准确地描述了它们实际的作用:

bullet

Guarantee Enforce at runtime that functions will only throw listed exceptions (possibly none).
保证在运行时强制函数仅抛出列出的异常(可能无异常)。

bullet

Enable or prevent compiler optimizations based on the knowledge that only listed exceptions (possibly none) will be thrown having to check whether listed exceptions are indeed being thrown.
启用或阻止编译器优化基于仅会抛出列出异常(可能无异常)的知识必须检查是否确实抛出了列出的异常

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.
这里我们可以更清楚地看到,与其让编译器通过假设只会抛出某些异常来进行优化,情况恰恰相反:编译器必须在运行时做更多工作来确保只有那些异常确实被抛出。

The Scoop on Exception Specifications
异常规范的内幕

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()"的含义就从标准的"检查我是否违规抛出,若无意抛出则阻止"变成了"相信我绝不会抛出,请据此优化"。务必注意:即使你选择使用空异常规范,也要阅读编译器文档并确认其真实行为——你可能会大吃一惊。保持清醒,谨慎驾驶。

Summary  总结

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…
但且慢,还有更多:同样的情况是否也适用于导出功能?关于这一点,我们下次回来再谈……

Notes  笔记

[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
版权所有 © 2009 Herb Sutter