C++模板代码的组织方式介绍

C++模板代码的组织方式介绍

说明:本文译自《C++ Template: The Complete Guide》一书的第6章中的部分内容。最近看到C++论坛上常有关于模板的包含模式的帖子,联想到自己初学模板时,也为类似的问题困惑过,因此翻译此文,希望对初学者有所帮助。

 

模板代码有几种不同的组织方式,本文介绍其中最流行的一种方式:包含模式。

链接错误

大多数C/C++程序员向下面这样组织他们的非模板代码:

·类和其他类型全部放在头文件中,这些头文件具有.hpp(或者.H, .h, .hh, .hxx)扩展名。

·对于全局变量和(非内联)函数,只有声明放在头文件中,而定义放在点C文件中,这些文件具有.cpp(或者.C, .c, .cc, .cxx)扩展名。

这种组织方式工作的很好:它使得在编程时可以方便地访问所需的类型定义,并且避免了来自链接器的“变量或函数重复定义”的错误。

由于以上组织方式约定的影响,模板编程新手往往会犯一个同样的错误。下面这一小段程序反映了这种错误。就像对待“普通代码”那样,我们在头文件中定义模板:

// basics/myfirst.hpp

#ifndef MYFIRST_HPP

#define MYFIRST_HPP

// declaration of template

template <typename T>

void print_typeof (T const&);

#endif // MYFIRST_HPP

print_typeof()声明了一个简单的辅助函数用来打印一些类型信息。函数的定义放在点C文件中:

// basics/myfirst.cpp

#include <iostream>

#include <typeinfo>

#include "myfirst.hpp"

// implementation/definition of template

template <typename T>

void print_typeof (T const& x)

{

std::cout << typeid(x).name() << std::endl;

}

这个例子使用typeid操作符来打印一个字符串,这个字符串描述了传入的参数的类型信息。

最后,我们在另外一个点C文件中使用我们的模板,在这个文件中模板声明被#include

// basics/myfirstmain.cpp

#include "myfirst.hpp"

// use of the template

int main()

{

double ice = 3.0;

print_typeof(ice); // call function template for type double

}

大部分C++编译器(Compiler)很可能会接受这个程序,没有任何问题,但是链接器(Linker)大概会报告一个错误,指出缺少函数print_typeof()的定义。

这个错误的原因在于,模板函数print_typeof()的定义还没有被具现化(instantiate)。为了具现化一个模板,编译器必须知道哪一个定义应该被具现化,以及使用什么样的模板参数来具现化。不幸的是,在前面的例子中,这两组信息存在于分开编译的不同文件中。因此,当我们的编译器看到对print_typeof()的调用,但是没有看到此函数为double类型具现化的定义时,它只是假设这样的定义在别处提供,并且创建一个那个定义的引用(链接器使用此引用解析)。另一方面,当编译器处理myfirst.cpp时,该文件并没有任何指示表明它必须为它所包含的特殊参数具现化模板定义。

头文件中的模板

解决上面这个问题的通用解法是,采用与我们使用宏或者内联函数相同的方法:我们将模板的定义包含进声明模板的头文件中。对于我们的例子,我们可以通过将#include "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一个使用我们的模板的点C文件中包含myfirst.cpp文件,来达到目的。当然,还有第三种方法,就是删掉myfirst.cpp文件,并重写myfirst.hpp文件,使它包含所有的模板声明与定义:

// basics/myfirst2.hpp

#ifndef MYFIRST_HPP

#define MYFIRST_HPP

#include <iostream>

#include <typeinfo>

// declaration of template

template <typename T>

void print_typeof (T const&);

// implementation/definition of template

template <typename T>

void print_typeof (T const& x)

{

std::cout << typeid(x).name() << std::endl;

}

#endif // MYFIRST_HPP

这种组织模板代码的方式就称作包含模式。经过这样的调整,你会发现我们的程序已经能够正确编译、链接、执行了。

从这个方法中我们可以得到一些观察结果。最值得注意的一点是,这个方法在相当程度上增加了包含myfirst.hpp的开销。在这个例子中,这种开销并不是由模板定义自身的尺寸引起的,而是由这样一个事实引起的,即我们必须包含我们的模板用到的头文件,在这个例子中是<iostream><typeinfo>。你会发现这最终导致了成千上万行的代码,因为诸如<iostream>这样的头文件也包含了和我们类似的模板定义。

这在实践中确实是一个问题,因为它增加了编译器在编译一个实际程序时所需的时间。我们因此会在以后的章节中验证其他一些可能的方法来解决这个问题。但无论如何,现实世界中的程序花一小时来编译链接已经是快的了(我们曾经遇到过花费数天时间来从源码编译的程序)。

抛开编译时间不谈,我们强烈建议如果可能尽量按照包含模式组织模板代码。

另一个观察结果是,非内联模板函数与内联函数和宏的最重要的不同在于:它并不会在调用端展开。相反,当模板函数被具现化时,会产生此函数的一个新的拷贝。由于这是一个自动的过程,编译器也许会在不同的文件中产生两个相同的拷贝,从而引起链接器报告一个错误。理论上,我们并不关心这一点:这是编译器设计者应当关心的事情。实际上,大多数时候一切都运转正常,我们根本就不用处理这种状况。然而,对于那些需要创建自己的库的大型项目,这个问题偶尔会显现出来。

最后,需要指出的是,在我们的例子中,应用于普通模板函数的方法同样适用于模板类的成员函数和静态数据成员,以及模板成员函数。