C++模板会不会增加冗余

2015-06-11 23:47:39 +08:00
 jjtx

在某个头文件中定义了模板函数,在其他的多个cpp文件中需用这个函数。比如下面的例子:

// a.h
template<class T>
void foo(T t) {
    // 这里有许多实现代码
}

// test.cpp
#include "a.h"
void test() {
    foo(1);
}

// test2.cpp
#include "a.h"
void test2() {
    foo(2);
}

这两个cpp文件编译后都会产生foo的函数体(实例化,模板参数 T 都是int),这两份函数体是一样的,这岂不是冗余?非模板的类和函数可以将实现分离,不存在这个问题。我查了下,发现将模板的接口与实现分离是不太好的,STL的接口与实现都是写在一起的。第二个问题:如果是这样的话,那这岂不是不得不“开源”了?不得不将实现给用户看。

964 次点击
所在节点    C
10 条回复
xyuf
2015-06-12 00:02:12 +08:00
有种猥琐的办法可以符合你这个需求
把原来的a.hpp拆成a.h和a.cpp,然后在a.h里写声明,a.cpp里把所有可能的都实例化出来
然后工程里加入a.cpp文件,在别的需要foo函数的地方一样只要include a.h即可
这样在工程编译的时候,a.cpp产生了a.o,里面包括了各种foo函数的代码,而别的使用foo的cpp文件产生的.o文件里只有调用foo的代码,没有foo的代码,让链接器完成之后的工作
但是这样写实在是没有什么意义,你不是研究编译原理的么?现代的主流编译器会做这种重复代码消除吗?
YuJianrong
2015-06-12 10:26:23 +08:00
1. 编译的时候是一定要产生冗余代码的,因为编译一个文件的时候不知道其他文件有没有
2. 编译的函数体可能是一样的也可能不是(不同h文件同名template或者同样template代码由宏定义有所改变),一样的时候大概linker会优化?
3. 模板接口与实现分离(exported template)虽然在标准中有不过没有编译器做吧
4. 模板本来就是暴露给使用者的(要不编译后的机器码哪里来),不可能像非模板一样只暴露接口。有这方面的顾忌就不应该使用模板
FrankHB
2015-06-13 02:03:41 +08:00
按照现在的编译器的一般实现基本不可能避免冗余。
不仅仅是模板,inline函数(不管最终是否被内联)、没有定义在翻译单元内的多态类的虚表(Clang++警告[-Wweak-vtables])也是这样。
然而因为One Definition Rule的要求,链接时必须去除这些在不同翻译单元中完全相同的代码生成的相同的外部链接的实体。所以链接完以后的映像中是不会允许这些重复的定义。坏处主要是编译效率低下以及浪费空间:做了很多重复的无用功。
根本原因是C++的翻译链接模型是对称的,没有一个中心翻译单元。没有额外的特殊约定,编译器每次翻译过程中直接生成的目标二进制映像只对这一个一个翻译单元有意义,再次编译其它翻译单元时之前已经翻译好的目标代码不会被复用。于是对于头文件这种在预处理后每个翻译单元都有重复的东西,就只能重复翻译了。而跨翻译单元的优化也得推迟到链接时。
FrankHB
2015-06-13 02:04:59 +08:00
@YuJianrong export只有EDG前端实现,而且C++11就移除了,关键字保留给以后用。
FrankHB
2015-06-13 02:17:53 +08:00
解决所有以上冗余的避免方法根本上都一样:把定义固定到确定的具体翻译单元中。然而这样会导致inline函数和模板的定义只有一个翻译单元能看得见,实际上没用。好在C++11引入了extern模板声明,允许显式指定不在当前翻译单元实例化,配合显式实例化就能基本消除这个开销(inline就看着办吧……)。
至于写头文件里会导致暴露实现,考虑到export这样的实现方式因为过于复杂、收益不够被毙了,是没办法解决的——根本上就是要标准化一种IL代替源码(反正得能让各家编译器都认识)表示实例化之前的东西,不可能是最终的目标代码,也就是五十步笑百步而已。
源码上只是要分离实现也不是不行,类似libstdc++这样把大段模板定义写到另外的文件里再包含,但技术上照样是头文件——只不过提醒用户没事别看罢了。当然写模板定义特别是类模板定义外的成员定义需要多几个template<>非常疼,自己看着办吧(反正我是没兴趣特别这样搞)。
FrankHB
2015-06-13 02:34:00 +08:00
……嘛,严格来说是否有重复编译的冗余还是得看你调用编译器的方式。
如果直接扔给编译器驱动所有源文件,比如g++ test.cpp test2.cpp -otest,这样至少理论上还是能够优化的,虽然我实际上没看到这样的优化实现能明显改善(-flto倒是会调用make……)。
而如果非得g++ -c test.cpp -otest.o; g++ -c test2.cpp -otest2.o然后ld或者g++链接这些.o的话,就没救了,因为现在几乎所有系统上这样都是让每次翻译在单独的进程里跑,而除了链接时-flto这样的黑科技外我还没见到有神到会自主IPC优化的C++编译器……
(然而大部分现实项目都是后者的方式分批构建的,所以效率嘛。。反正make -j是救不了的。)
YuJianrong
2015-06-15 10:44:23 +08:00
@FrankHB 很久不做C++,求教一下现代编译器如果不同源文件编译出来的目标文件中同样的末班函数不一样会怎么处理(比如模板中有宏,不同源文件有所差别)?是抛弃掉其中一个(结果不正确)?还是报错呢?还是重复链接进去?
FrankHB
2015-06-17 00:10:59 +08:00
@YuJianrong 简而言之同一个翻译单元内违反ODR一般就是编译错误,然而不同翻译单元之间的不匹配并不要求检查(no diagnostics required),所以可能链接出错也可能啥事没有但是就错的(ill-formed),不能指望什么有合理的结果,于是炸了也活该——这部分相当于C的undefined behavior的一个子集(事实上ISO C也就直接明确类似的违反就是UB了)。于是实际上没法保证是否有这些情况,虽然我用过的靠谱的工具链一般是链接错误。
jjtx
2015-07-06 10:24:41 +08:00
@YuJianrong 第四点,你的意思应该是”物理“(得把那些模板代码实例化)上必须暴露,这个我同意。我的意思是逻辑上不应该暴露,比如商业库,当然不想暴露实现啊。
YuJianrong
2015-07-08 18:42:53 +08:00
@jjtx 不是啊,我的意思不是实例化的机器码暴露,而是源代码(也就是你说得逻辑)对于模板来说就是必须暴露的。
原因很简单,编译器都还不知道使用者要用什么类型代入到模板里面去呢,怎么可能预先向使用者提供实例化的机器码呢?只有把使用者要用的类型代入带模板源代码中,取代掉模板定义的那些占位符,生成普通的C++代码,这个时候才能够编译成机器码,所以模板是必须暴露给使用者的。
其实模板也可以看做是#define的升级版,你拿到一个lib再怎么改头文件的#define,编译好的机器码也不会再变了,只有拿到源代码才能编译出一个不一样的lib……

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/197881

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX