文 / 李博(光宇广贞)
《上篇文章》聊到了 VS 2010 对 C++0x 的支持在运行期强化上的不佳表现,首先是 IDE 支持得不够理想,内核上对于悬而未决的 constexpr 则直接否决,不像对 export 至少还给点儿面子。本篇聊建构期强化。主题是“外部模板”。
我在《C++ 中 export 关键字的尴尬处境》这篇文章里聊到了“分离编译模式”在模板(template)上的无奈。为此,现行标准 C++ 98/03 设计了 export 关键字,用 export template 以实现模板的分离编译模式。但由于这一机制在实际操作中的恶心表现,现有包括 VS 和 GCC 等号称“百分百支持标准”的编译器在内的绝大多数编译器供应商都强烈抵制这一机制。便是支持这一机制的 Intel C++ 编译器默认支持选项也是关闭状态,开启后编译时也会针对 export template 的使用给出警告。
C++ 标准委员会《N1448》、《N1960》、《N1987》三个文件提案添加“extern template”机制,并将之补充至新扩充的语言内核当中。关于新标准的背景介绍及本文将采用的实验平台信息等请参阅《测试 VS 2010 对 C++ 0x 标准的谨慎支持》。
首先我们要简单了解一下编译器处理模板代码过程中遇到的问题。
当下自动实例化模板的通用技术是,编译器(Compiler)先将各个翻译单元(translation unit)引用到的特化模板函数实例化,然后链接器(Linker)将不同翻译单元中重复实例化的复本一个个消灭掉,最后使得同一程序的多个目标文件(obj files)只会保留某个特化模板函数的唯一实例,从而达到缩减程序文件大小的目的。达成这一目的需要昂贵的编译时间成本,而高昂的代价却并没换回完美的清理效果,链接器会因为一些原因消灭不净,仍然会生成臃肿的程序文件……
这里面一个很大的问题在于,程序员无法控制特化模板函数何时实例化,这些是由编译器根据代码来安排的。比如如下代码:
第一句只会实例化默认构造函数和析构函数,第二句只会实例化 push_back 方法。编译器看到哪句,便会执行该句对应的操作。若其它翻译单元又出过一次 push_back 方法,编译器会在那处再实例化一次,从而造成冗余。回想 C++ 98/03 时代,为了解决这一问题,C++ 标准先制定了“强制特化模板函数实例化语法”。即:
如代码所示,该语法将强制编译器在当前代码处实例化特化模板函数,而不是等到函数被调用时。但事儿不能做一半儿就算完了。你能告诉编译器何时强制实例化,也得有办法告诉编译器何时避免实例化,这样才能让程序员完全控制模板实例化操作,从而达到编译优化,实现一处编译多处引用。由此终于引出我们本文的主题—— extern template 机制。
《N1987》提到,目前已有编译器允许程序员通过显式声明 extern template 以抑制编译期内的隐式实例化。该文件明确了 extern template 的语义。现以原文附于下:
前三个意思说得都非常明白了,但请注意一下最后一条并未保证使用 extern template 可以控制内联函数的实例化,仅仅是用了“encourage”这样一个说了也等于没说的词。然而事实上很难做到。委员会明确指出 extern template 并不支持抑制内联函数的实例化,否则这将会给内联函数在调用处的展开带来问题。说得更明白一些,未在本翻译单元编译通过的内联函数如何立即在本单元内展开优化呢?难道要等到其它所有翻译单元都编译通过之后,才能确定从何处 extern 进来内联函数的实体,然后再继续本单元的编译么?——这甚至会带来死锁的问题。因此,代价昂贵且不安全。
上述可见 extern template 的使用仍然存在未解决的问题。事实上,该机制也未获标准委员会的明确通过。提案修改停留在 2006 年 4 月 6 号。而 VS 2010 则并未实际支持这一机制。举个小例子,引用 <vector> 头文件,并做如下声明:
编译器会报如下警告和错误,nonstandard!哈哈,很有意思:
这是使用标准库的情况,自定义模板类和模板函数的问题更多,报的都是些乱七八糟的错误,比如“>”符不对称之类的……
当前并未从微软 VC 官方博客找到关于 VC 2010 下 extern template 的任何消息。本来么,模板这一块,就是很麻烦。它是 C++ 令人心动之处,也是 C++ 令人恶心之处……
export 的尴尬处境
参考:
《测试 VS 2010 对 C++ 0x 标准的谨慎支持》
《C++ 0x(C++ 09)新标准全部革新提案文档列表》
《维基百科:C++ 0x》
《State of C++ Evolution (after Portland 2006 Meeting)》