【C++ 类模板】声明和定义要在同一文件中

一、引言

通常C++代码会分成.h.cpp进行编写。
其中.h放类和方法的声明,提供给其他文件包含。而.cpp中放方法的定义。
这样分离式编译的优点自然是逻辑清晰,类的结构在.h中一目了然,而且在编译时也进行了解耦(非本文重点,此处略)。

而对于类模板,即类型参数化了的类,例如 STL中的容器类,如果按照上述.h.cpp分离的方式编写,会导致编译不通过

举个栗子:

demo.h 中声明个Demo类和构造方法

template <typename T>
class Demo
{
    public:
        Demo(T i);
    
    private:
        T m_i;
};

demo.cpp 中实现 Demo类的构造方法

#include "demo.h"

template <typename T>
Demo<T>::Demo(T i): m_i(i)
{
}

然后在main函数中创建个Demo对象

#include "demo.h"

int main()
{
    Demo<int> a(1);
    return 0;
}

按照以往的习惯,看起来似乎没什么不对,接下来编译一下
g++ main.cpp demo.h demo.cpp, 报错了:

/usr/bin/ld: /tmp/ccNJit1i.o: in function `main':
main.cpp:(.text+0x15): undefined reference to `Demo<int>::Demo(int)'
collect2: error: ld returned 1 exit status

在链接的时候找不到Demo<int>类型的构造方法.

二、怎么办

先说解决方案:

目前来说,由于编译器不支持类模板的分离式编译
最简单的解决方案就是将类模板的声明和定义放在同一文件中。像STL里做的那样。

三、为什么

再说为什么:为什么编译器不支持类模板的分离编译?


这要先说说 C++类模板的实现方式

类模板不等同于类的定义,而是用于生成具体类代码的代码,生成类代码的过程也就是模板的实例化
因为类模板可以实例化出各种类型的类代码,所以在编译时,出于性能考虑,会根据程序中实际用到类模板的地方,生成指定实例化类型的类代码。

举个栗子:

demo.h 中声明并定义Demo类的构造和func方法

template <typename T>
class Demo
{
public:
    Demo(T i) : m_i(i) {}
    void func() {}

private:
    T m_i;
};

main.cpp中创建两个Demo对象,其中一个调用了func方法

#include "demo.h"

int main()
{
    Demo<int> d1(1);
    d1.func();
    Demo<double> d2(2);

    return 0;
}

然后编译 g++ main.cpp demo.h -o template.out 生成可执行文件template.out

接下来为了更直观,会使用objdump反汇编工具一探这个.out文件的究竟。(可以通过objdump -h查看选项说明,此处略)
执行objdump -SC template.out > template.asm,然后打开template.asm,大致浏览一下,会看到有这么三个方法的反汇编代码:

template.asm

这就是编译器通过类模板实例化所生成的代码。
分别是Demo<int>类的构造、func方法和Demo<double>类的构造方法,而没有Demo<double>类的func方法,
不难猜测,这是因为在main函数中,Demo<double>对象实际并没有调用func,如果生成该方法会产生冗余代码,这是C++所不能容忍的。

通过此番查看,证实了一个想法:编译器只会根据实际使用到的模板参数类型和方法,对类模板进行实例化,即生成类代码。


再说C++的分离编译

C++ 代码是怎么分离编译的?
由于编译这个话题很深,涉及很广,此处只大致描述,细节推荐读物 《深入了解计算机系统》 《程序员的自我修养 : 链接、装载与库》

每个cpp文件为一个编译单元,C++代码从一堆cpp文件到一个可执行程序的过程,可以认为大致分为三步:

1. 预处理
这步主要是把宏和#include替换掉,
对于每个编译单元,即每个.cpp中,会把由#include指定的每个.h文件的内容都放到cpp文件的顶部,替换掉原来的#include语句。

2. 编译
依旧是对于每个编译单元,会进行单独编译,各自首先将C++代码转换成汇编代码,然后转换成相应的.o二进制文件。
可以发现,直到第二步编译完成,生成.o文件, 各cpp文件之间是没有交集的,一直在单独处理各自文件中的代码,
并不知道其他.o中是否使用了自己定义的方法,或者自己使用了的.h中声明的方法是否在其他.o中被定义

3. 链接
链接就是把所有.o连接到一起,生成最终可执行程序的过程。
对于每个.o中引用了其他.o中的方法,会在这个阶段确定具体的函数跳转地址。
如果有.o中使用的方法在其他所有.o中都没有定义,则会报错未定义的引用,并无法生成可执行文件。

这就是C++分离编译的大致流程。


所以,为什么不支持类模板分离编译?

现在回头看引言中的栗子:

demo.h 中声明了个Demo类和构造方法

#pragma once

template <typename T>
class Demo
{
    public:
        Demo(T i);
    
    private:
        T m_i;
};

demo.cpp 中实现 了Demo类的构造方法

#include "demo.h"

template <typename T>
Demo<T>::Demo(T i): m_i(i)
{
}

然后在main函数中创建个Demo对象

#include "demo.h"

int main()
{
    Demo<int> a(1);
    return 0;
}

现在再考虑编译时发生了什么,在执行g++ main.cpp demo.h demo.cpp 进行编译时, demo.cpp会被视为一个编译单元,引入了demo.h,单独进行编译。
但是在这个编译单元中,并没有使用这个类模板,即不会由编译器对类模板进行实例化,不会生成任何参数类型的Demo类代码
所以在链接阶段,main.cpp这个编译单元所生成的main.o,与demo.cpp这个编译单元所生成的demo.o文件进行链接时,找不到Demo<int>::Demo(int)方法的定义,这就是引言中报错信息undefined reference to Demo<int>::Demo(int)的根因。

那么真的是这样吗?验证一下,将demo.hdemo.cpp编译成demo.o,看看里面是不是真的没有定义任何方法。
执行g++ -c demo.h demo.cpp -o demo.o ,其中加上-c选项,就只编译不链接。
然后像反汇编.out文件一样查看demo.o文件,执行objdump -SC demo.o > demo.asm
可以发现,demo.asm文件中确实什么定义都没有。

demo.asm

所以现在C++编译器的编译机制,确实不支持类模板的分离编译


QA:

那为什么声明和定义写在同一个文件中就可以?

不难理解,如果声明和定义写在同一个文件中,被#include进使用了该类模板的其他.cpp文件,类模板就可以和使用了它的方法处在同一个编译单元,在编译时完成模板的实例化。

那如果非要类模板声明和定义分离,自己单独一个编译单元,有什么办法吗?

有。在类模板的.h.cpp中,调用类模板,在该编译单元中,提前实例化外部需要的类型和方法。
当然,前提是确认了外部所有要使用该类模板的实例化类型,与具体调用的方法,并在后续项目扩展时如果实例化了新类型,也需要在这个编译单元中提前调用进行实例化。


四、结语

C++依旧在发展,C++编译器也依旧在发展,虽然现在普遍不支持类模板的分离编译,但是说不定,下一版C++标准中就提出了类模板分离编译的解决方案,这篇文章中的很多内容自然也就过时了,共勉 ~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容