C++委员会投票记事之弗吉谷
Sean A. Corfield
ALNG译
宾夕法尼亚州的弗吉谷(Valley Forge)是数次著名战役的战场。 最近的一次X3J16/ WG21会议于1994年11月在它那位于几条高速公路之间的会议中心召开。大约是为了体现此地的精神,C++委员会中的几个派别间干了几场小仗。很不幸,这些阵仗只是为了几个语法上的小问题而已,其中掺杂了不少政治意味在内。
历史这样被创造出来
然而,当委员会通过投票决定从标准库草案中删除掉一些部分内容时,历史就这样被创造出来了。但又有谁听说过这样的事呢?我们都知道标准委员会只会增加东西,不是吗?在委员会的这次表决中(即删除草案中部分内容)包括了对容器类以及流类中的一些“巴洛克式”部分的早期尝试。
随着STL在1994年7月的引入, 我们现在拥有了一个可以用来支撑整个库的一致性框架,并正致力于把库的其他部分改造成为STL风格:将字符串及流模板化,增加迭代器并使类接口整体上更一致。 STL中提供了容器vector,这使得位串(bit_string),动态数组(dyn_array)以及动态指针数组(ptr_dyn_array)变成多余(因为他们可以分别为vector,vector和vector所替代而无损于功能)。
委员会对流库也作了一些小手术,去除掉了stdiostream而改用fstream。 stdiostream的本意是用来辅助程序员混合使用stdio/stream进行编程,但是fstream的语义已逐步改变,现在这样的帮助已全无必要。流库成长过程中的另一个受害者是strstream(流世界的sscanf / sprintf),它现在已不被赞成使用(即:被标注为在标准的未来版本可以考虑被删除掉的潜在目标),因为stringstream可以提供一种更安全使用类string的替代方法。
关于异常的斗争
英国人提出了反对意见,他们说异常差不多破坏了每个程序。如下的代码片段很好地展示了异常是如何把事情搞乱的
void f()
{
T* p = new T;
// 一些处理
delete p;
}
如果处理过程引发异常抛出,那么语句“delete p;”便不会被执行,这就导致了内存泄漏。 为了让上面的代码具有“异常安全性(exception-safe)”,我们就得采用与资源管理相关联的“获得即初始化(initialisation is acquisition)”惯用法:
class T_handle
{
public:
T_handle(T* pp)
: p(pp) { }
~T_handle()
{ delete p; }
operator T*()
{ return p; }
private:
T* p;
};
void f()
{
T_handle p(new T);
// 一些处理
// 当离开作用区域或
// 异常被抛出时,
// P会被自动删除掉
}
英国人将这种做法视为一张基本的惯用法。这使得我们在草案登记投票中投赞成票前需要找到一个解决方法(投票正在进行,其详情可参参CVu7.1中的 "The Casting Vote")。 Greg Colvin提出了几个提议,其中涵盖了“智能指针”和垃圾收集。为了使英国人以及其他ISO成员满意,委员会最终采纳了其中的一个提议。这个用法与上例相似的类模版被称为auto_ptr。Greg Colvin同时还建议使用counted_ptr,它可以用作引用计数对象的基础。但是库工作组的主席在向委员会其他成员介绍这个类时的描述不太让人信服,并且他还不赞成所谓的“ISO敲诈”(即对其他特性或函数库成员投赞成票的结果取决于委员会是否接受另外的某个语言特性或库类)。正如我以前说过的那样,ANSI委员会的几个成员也持相同的观点,这样要想取得国际共识就更难了。最终的结果是ANSI对counted_ptr投了反对票,而ISO赞成接受counted_prt。经过一些增进共识的政治操作之后,一些国家改变了他们的投票方向。最终该提议被撤回,但同时也造成了在1995年3月德克萨斯Austin的再次表决。委员会内部的团体之争还在继续,但至少其政治意味减轻了不少。
异常合约
异常规格(exception specifications)能有什么用? 答案可能是:“不大有用”。我们虽然可以在函数声明中指明该函数会抛出什么样的异常,但我们又可以在多大程度上信任这样的声明呢?在Valley Forge[本次投票地点,译注]之前, 答案是:“很少”。这是因为当编译器看到了一份异常规格时,它就得产生必要的代码来捕获其他的异常,并调用unexpected()。而函数可能抛出它所设想的任何异常,这样也就破坏了函数异常规格所制订出的合约。这也是英国人所指出的另一个bug。对于这种情况的可能的解决办法包括:从语言中去掉异常规格;或使异常规格可以在编译时被检查,不过这两者都没有得到委员会的普遍支持。最后语言特性扩展工作组提出了一个容易为大家所接受的折衷方案:在unexpected()中不能抛出任何违反合约的东西。这意味着编译器可以对异常规格进行合理的检查,并在规定不可调用unexpected()处做一些优化处理。这样就使得使用异常规格变得有吸引力得多,似乎同时就满足了几个不同的团体。
模板——他们应该放在哪?
在书写可移植的C++程序中最困难的地方可能就是如何组织模板代码以使得其能在多个平台上编译。(对Microsoft C++程序员来说,只要不是在NT上运行VC++2.0的话,那就用不着担心这些)。对Borland程序员来说,这都是预先确定好了的:你只需包含所有你的定义,编译器帮你厘清。对CFront程序员来说,它也有良好的定义:如果声明部分在x.h中的话,那么定义部分就在x.c中。所有的一切都显得很好,没有任何问题,但一旦你试着在上述两个环境之间移植代码时,你就麻烦大了。所以对多平台工具提供商来说,这是一个真实而昂贵的问题。
模板编译是英国人所强烈关注的另一个问题,扩展工作组也一直在致力为此寻找一个可能的解决方案。确切地说,是Bjarne Stroustrup本人对此孜孜以求。在此此前的最佳结果是一个新的指令,它告诉编译器可以到哪去找模板的定义。这也是一种折中方案,它既满足了一些编译器厂商所偏爱的预处理指令,也给以其他人(和大多数用户)一个他们所喜欢的完全自动化或者说魔术般的系统。最终采用的方案意味着模板的行为和语言其他部分大致相似。声明被放在头文件中而定义则被放在在源文件中。因为其行为方式与非模板如出一辙,如果需要其为静态[static]或内联[inline]的话,我们也可以将定义放入头文件中。这样用户使用固然容易,但厂家实现起来就要大费周章了。希望我们大家都能够从这一(重大)变更中得益。
explicit
正如你所预料那样,我们对另一关键字进行了表决。在叫嚷之前,先让我回过头给点背景资料。你是否经常碰到单参构造函数(a constructor with one argument)?在需要时,隐式类型转换真是妙不可言,不过它也常常在预料不到的地方冒了出来。考虑如下的代码片断:
void f(string);
f(5);
吃惊吗?这个调用完全合法,因为存在从5到string的隐式转换。那它都干了些什么勾当呢?是创建一个5个元素的string?还是一个值为5的单字符string?要想得到正确的答案,就必须清楚地了解这个类,而这种情况可能很常见。 类设计者如何才能避免这些隐式的转换呢?目前常用的技巧是加一个需要用户指定的哑元参数(dummy argument)或者使用中间转换类型。这两个办法都很“聪明”,但也都很“丑陋”,它们都具有我们所不想要的语义。 Nathan Myers提议增加constructor关键字来指明非转换构造函数(为了对称,也提议了destructor 关键字)。这个提议总体上很受欢迎。 实际上德国早就将非转换构造函数问题作为其反对票问题之一。 不幸的是提议在全体委员中付诸讨论时有几个人对其语法提出了异议。我们一遍遍考虑了所有有趣的建议,最后委员会决定接受单个关键字explicit。我们可以将一个构造函数声明为explicit,这样它再也不能用于隐式类型转换。
哇!没有涉及内核?
当然,语言核心工作组很忙! 这次他们主要处理了对象形态获取[object shape acquisition]以及对象的常数性这两个问题。他们还重审了构造期间的多态行为。如果你对此主题很感兴趣,请通过e-mail和我探讨。但我可不想公然跳入这样一个蛇坑!
下集预告
下次投票记事专栏将讲述1995年3月在德州Austin发生的事。我们将了解CDR投票的结果,然后我应该就可以报道是否开始CD投票了。
Sean A. Corfield
本文地址:http://com.8s8s.com/it/it23339.htm