模板是一个 C++ 的高级特性,它使得程序员可以编写一个类或者函数,以相同的方式处理不同类型的数据。
前段时间在网上看到,有人认为类模板的成员函数必须实现在类模板的定义当中。这与我一直以来的认知是冲突的——我认为类模板和普通的类其实没有什么差别,除了它需要做额外的实例化之外。于是,我翻看了 C++ Primer,并检索了一些资料,就有了这篇文章。
模板编译
C++ 中的模板,是一个很有意思的概念。说它有意思,是因为这个模板的概念,和现实生活中的模板非常相似。因此,实际上只需要理解了现实生活中的模板,理解 C++ 中的模板的一些特殊性就容易了。
学生在写论文的时候,经常会去网上检索「LaTeX 论文模板」。下载下来之后,论文模板实际上只有一个简单的骨架。学生们需要在这个骨架的基础上,书写、构建自己的内容,才能变成实际的论文。这个过程,我们称其为「实例化」;这是一个从一般到特殊的过程——从一个一般化的模板,填入自己的内容,变成自己的论文。
C++ 中的模板也是一样。当程序员定义了一个模板后,它只是一个骨架。而只有当编译器知道用户需要传入的模板类型参数之后,编译器才会给出一个「有血有肉」的函数(对应函数模板)或者类(对应类模板)。因此,对于模板的编译,我们需要知道:当我们使用模板时(具体来说,是实例化时),编译器才会根据模板的定义生成具体的代码。这个特性,决定了涉及到模板的错误检测时机可能与一般的代码有所差异。
类模板的特殊性
对于普通的类,当我们使用这个类时,编译器必须能够知道类的定义。但是,类中的成员函数,则不必在编译时就能被编译器读到——可以在链接的过程中,再确定具体的类成员函数。因此,一个通常的做法是:在头文件中进行类的定义和类成员函数的声明;而在源文件中实现类的成员函数。
对于类模板来说,当我们使用这个类模板时,会传入模板类型参数,而后编译器会根据类模板和传入的类型参数生成类对应的代码。因此,此时编译器必须能够知道类的定义,也必须知道类中成员函数的实现。类模板的这一特殊性,使得在处理类模板相应的实现的时候,通常会把类模板的定义和成员函数的实现都放在头文件里。
在类模板的定义之外实现成员函数
与普通的类相同,程序员既可以在类定义的内部实现成员函数,此时这些成员函数会被隐式地声明为 inline
的;程序员也可以在类定义的外部实现成员函数。不过,对于后者,会有需要注意的地方。
类模板的成员函数必然是函数模板
这是因为,类模板的成员函数和类模板共享模板参数;并且,类模板的每个实例,其成员函数都是互相独立的。因此,哪怕类模板的某个成员函数,完全不会用到模板参数,它也必须是一个函数模板,并且模板参数和类模板的参数保持一致。
在类模板实例化时,类模板的成员函数必须是可见的
这个原因,在上一节已经讲过了。
两种做法
至此,我们可以讨论一下,在类模板定义的头文件之外实现其成员函数,具体要怎么做了。一般来说,有两种做法。
在头文件末尾引入实现源文件
这样一来,成员函数的实现会被 #include
包含进头文件,实际上在编译器看来和写在头文件内没有本质的差别了。
1 |
|
1 | template<typename T> |
在实现时显示地实例化类模板
这种情况下,只有实例化的版本才能被使用——因为只有它们真正被编译成了类代码;其它未实例化的版本,实际是没有定义的类。
1 |
|
1 |
|
当编译过 bar.cpp
之后,其它包含头文件 bar.h
并与其目标文件链接的源文件,就可以使用 Bar<int>
和 Bar<double>
这两个类了。而使用其它版本,则会报错。