C++templates第九章在实践中使用模板

包含模式

链接错误

大多数 C 和 C++程序员都会按照如下方式组织代码:

  • 类和其它类型被放在头文件里。其文件扩展名为.hpp(或者.h,.H,.hh,.hxx).
  • 对于全局变量(非 inline)和函数(非 inline),只将其声明放在头文件里,定义则被放在一个当作其自身编译单元的文件里 。这一类文件的扩展名为.cpp( 或 者.C,.c,.cc,.cxx)。

这样做效果很好:既能够在整个程序中很容易的获得所需类型的定义,同时又避免了链接过程中的重复定义错误。

受这一惯例的影响,刚开始接触模板的程序员通常都会遇到下面这个程序中的错误。和处理 “常规代码”的情况一样,在头文件中声明模板:

1
2
3
4
5
6
7
#ifndef MYFIRST_HPP 
#define MYFIRST_HPP
// declaration of template
template<typename T>
void printTypeof (T const&);

#endif //MYFIRST_HPP

其中 printTypeof()是一个简单的辅助函数的声明,它会打印一些类型相关信息。而它的具体 实现则被放在了一个 CPP 文件中:

1
2
3
4
5
6
7
8
9
#include <iostream> 
#include <typeinfo>
#include "myfirst.hpp"
// implementation/definition of template
template<typename T>
void printTypeof (T const& x)
{
std::cout << typeid(x).name() << ’\n’;
}

这个函数用 typeid 运算符打印了一个用来描述被传递表达式的类型的字符串。该运算符返回一个左值静态类型 std::type_info,它的成员函数 name()可以返回某些表达式的类型。C++ 标准并没有要求 name()必须返回有意义的结果,但是在比较好的 C++实现中,它的返回结果应该能够很好的表述传递给 typeid 的参数的类型。

接着在另一个 CPP 文件中使用该模板,它会 include 该模板的头文件:

1
2
3
4
5
6
7
#include "myfirst.hpp"
// use of the template
int main()
{
double ice = 3.0;
printTypeof(ice); // call function template for type double
}

编译器很可能会正常编译这个程序,但是链接器则可能会报错说:找不到函数 printTypeof() 的定义。

出现这一错误的原因是函数模板 printTypeof()的定义没有被实例化。为了实例化一个模板, 编译器既需要知道需要实例化哪个函数,也需要知道应该用哪些模板参数来进行实例化。不幸的是,在上面这个例子中,这两组信息都是被放在别的文件里单独进行编译的。因此当编译器遇到对 printTypeof()的调用时,却找不到相对应的函数模板定义来针对 double 类型进行实例化,这样编译器只能假设这个函数被定义在别的地方,然后创建一个指向那个函数的引用(会在链接阶段由链接器进行解析)。另一方面,在编译器处理 myfirst.cpp 的时候,却没有任何指示让它用某种类型实例化模板。

头文件中的模板

解决以上问题的方法和处理宏以及 inline 函数的方法一样:将模板定义和模板声明都放在头文件里。

目前有几个问题需要指出。最值得注意的一个是,这一方法将大大增加 include 头文件 myfirst.hpp 的成本。在这个例子中,成本主要不是由模板自身定义导致的,而是由那些为了 使用这个模板而必须包含的头文件导致的,比如。由于诸如 的头文件还会包含一些它们自己的模板,因此这可能会带来额外的数万行的代码。

尽管有编译时间的问题,但是除非有更好的方法,我们建议在可能的情况下还是尽量使用这一方式来组织模板代码。在写作本书的 2017 年,有一个正在准备阶段的机制: modules(C++20 已落实)

模板和inline

提高程序运行性能的一个常规手段是将函数声明为 inline 的。Inline 关键字的意思是给编译器做一个暗示,要优先在函数调用处将函数体做 inline 替换展开,而不是按常规的调用机制执行。

预编译头文件

即使不适用模板,C++的头文件也会大到需要很长时间进行编译。而模板的引入则进一步加剧了这一问题。

预编译头文件方案的实现基于这样一个事实:在组织代码的时候,很多文件都以相同的几行代码作为开始。为了便于讨论,假设那些将要被编译文件的前 N 行内容都相同。这样就可以单独编译这 N 行代码,并将编译完成后的状态保存在一个预编译头文件中(precompiled header)。接着所有以这 N 行代码开始的文件,在编译时都会重新载入这个被保存的状态, 然后从第 N+1 行开始编译。在这里需要指出,重新载入被保存的前 N 行代码的预编译状态可能会比再次编译这 N 行代码要快很多很多倍。但是保存这个状态可能要比单次编译这 N 行代码慢的多,编译时间可能延长20%到 200%。

因此利用预编译头文件提高编译速度的关键点是;让尽可能多的文件,以尽可能多的相同的 代码作为开始。也就是说在实践中,文件要以相同的#include 指令(它们可能占用大量的编 译时间)开始。因此如果#include 头文件的顺序相同的话,就会对提高编译性能很有帮助。

所以我们应该多#include 一些可能用不到的头文件。这样做可以大大简化预编译头文 件的使用方式。比如通常可以创建一个包含所有标准头文件的头文件,称之为 std.hpp:

1
2
3
4
5
6
#include <iostream> 
#include <string>
#include <vector>
#include <deque>
#include <list>

这个文件可以被预编译,其它所有用到标准库的文件都可以直接在文件开始处 include 这个头文件:#include "std.hpp"