c++模板编程应该把实现放在头文件中吗,这样写会不会让头文件变得很难看?

关注者
63
被浏览
50,104

15 个回答

理论上,模板函数的实现应该放在头文件里。

这是因为 C++ 继承了 C 语言先分离编译最后链接的传统,而模板的实例化在编译期,所以必须要让编译器看到函数的实现,才能实例化。

关于这个问题,C++ 标准也不是没想过要改,反而是早在 C++98 的时候就想过了,想把模板实例化移到链接期间——这样实现就不用写在头文件里了,多美好啊!!!

不过,结果就是,搞了个 export 关键字来导出模板,但却受到了各大主流编译器厂商的联合抵制,基本没几个编译器鸟它。到现在如果你用 gcc11.1 编译器,开启 -std=c++03 选项,仍然能看到这样一行略带嘲讽语气的警告:

大概就是说,“export 我懒得实现,所以就算你写了 export 我也不管”

至今除了 comeau C++ 等少数编译器外,没有几个编译器实现了这个功能,原因就是,把模板实例化移到链接期需要在编译期生成中间语言等一系列不和谐的东西,难度过大。最终 C++ 标准委员会无奈在 C++11 中弃用了 export 关键字。

(ps:现在 export 关键字被 C++20 用来导出模块了~


不过呢,C++11~C++17,这段时间,C++ 一直在向推行 header-only 方向努力,也就是一律写在头文件里(包括 constexpr 变量、inline 变量等等),inline 函数可以保证所有函数都可以写在头文件里,而 inline 变量则使得全局变量也能写在头文件里了。总之是在打破之前头文件要写一遍声明,cpp里要写一遍实现的复杂之处。

不过 C++20 推出了 module 特性,要尽可能地避免头文件这种拖慢编译速度的设定………听起来很美好。不过……用起来可以说是一言难尽了…确实该有的语法功能都有,但是连标准库都没有模块化,模块和标准库头文件混用,总是带来一些迷之重定义问题~还得等 C++23 给收拾烂摊子。

(顺便吐槽一句,C++20 出的特性基本上都是该出的倒是都出的,但是只出了一半…

首先从技术上讲一讲为什么模板实现(其实准确的说法叫定义)必须放在头文件里。

C++编译器在编译代码时是以cpp为单位的,常把cpp叫做 翻译单元 。至于头文件,在预处理的时候都以文本复制的形式变成cpp的一部分了。cpp文件里如果使用了模板(实例化),那么在它之前必须要有模板的定义(实现)。这和非模板函数不同,函数使用时只要有声明,不用有定义。

因为上面的限制,如果你的模板要给多个cpp使用,那就必须保证这些cpp都能看到你的模板定义(而不只是声明)。当然,如果你脑洞够开,你也可以把同一个模板的定义复制多份分别放到不同的cpp里面去。但我必须要说的是, 这是极其危险的做法!

当C++编译器编译每一个翻译单元时,它会将实例化的模板记录下来。然后在链接阶段,比对所有翻译单元中是不是用到了相同的模板实例(比如 f<int>() ),最后把所有相同的模板实例合并成一个,合并的时候,它会 随机 的保留某一个翻译单元中的定义,把其他的全部丢弃。

好,现在你可以想象,如果一旦不同翻译单元中,同一个模板的定义不一致会发生什么了。注意在整个过程中,编译器不会有任何告警和错误提示。

下面再从代码可维护性角度来说说定义是放在头文件好还是cpp文件中好。

C和C++的头文件是编程语言界一大奇葩特性。在C/C++之外,我没见过任何现代编程语言有头文件这种东西。头文件给代码工程的维护带来了很大的麻烦:接口和实现的版本要管理一致;模块的对外发布难以用包提供;编译时间被延长……

C++提供了inline函数后,程序员发现利用inline来写函数,可以像Java那样不用再有个另外的cpp了,函数头和参数终于不用写两遍了。于是,inline函数被用得越来越多。以至于后来为了发扬光大,C++17还弄出了inline变量。C++11中的constexpr函数也是默认inline的,你不想把它放头文件都不行。

最后,C++20一举收网,干脆推出了头文件的彻底替代品modules。使用最新的C++20编译器,项目已经可以不需要任何include指令(除了标准库以外)。

modules带来的好处有多大呢?C++之父 Bjarne 在去年的大会上,演示了一段代码,使用modules之后,编译速度比头文件快了20倍。

好了,现在历史潮流向着哪儿已经很清楚了。剩下的问题是,我们应该怎么做?