[翻译]如何组织使用模版技术的源代码

如何组织使用模版技术的源代码
——在C++模版库中几种不同的组织源代码的方法

作者:Nemanja Trifunovic
译:Weily

引言
常常有人问我,使用模版技术来写程序难不难?一般我都回给出这样的答复:“ 使用模版是简单的,但是, 构造模版是困难的。”只要稍微看一下我们平时写程序常用的那些模版库,例如STL、 ATL、 WTL和Boost中的一些库,你就会明白我的意思了。这些模版库都是说明下面这个原则的很好的例子——“简单的接口,复杂的实现”。

自从五年前我知道了MFC模版容器(template container),我就开始使用模版了。直到去年为止,我都不用自己去开发模版。当我最终需要自己构造一些模版类时,我遇到的第一个麻烦就是“传统”的源代码组织方式对于模版技术来说,并不管用。然后我花了不少时间去研究为什么这种方式碰到了模版就不管用了,也考虑了如何解决这个问题。

本文针对的是那些已经理解了如何使用模版,但在开发模版方面缺少经验的开发人员。这里我只介绍了模版类的相关内容,而不介绍模版函数的相关部分,因为对于这两种情况而言,原则上是一样的。

问题描述
为了演示这个问题,我们将使用下面这个例子。假设我们在array.h里有一个模版类array(与boost::array没有任何关系)。

// array.h
template
class array
{
? ? T data_[SIZE];
? ? array (const array& other);
? ? const array& operator = (const array& other);
public:
? ? array(){};
? ? T& operator[](int i) {return data_[i];}
? ? const T& get_elem (int i) const {return data_[i];}
? ? void set_elem(int i, const T& value) {data_[i] = value;}
? ? operator T*() {return data_;}? ? ?
};


并且,我们在main.cpp文件中使用了array这个模版类:

// main.cpp
#include "array.h"

int main(void)
{
array intArray;
intArray.set_elem(0, 2);
int firstElem = intArray.get_elem(0);
int* begin = intArray;
}


这个能够很顺利地通过编译,并且执行的结果也是完全在意料之中的:首先,我们构造了一个具有50个整数的数组(array),然后将第一个元素的值设为2,再读出这个元素。最后将指针指向数组的起始位置。

现在,让我们看看,如果试着使用传统的代码组织方式,将会发生什么?我们把array.h文件中的代码分离出来,这样,我们就有了2个文件:array.h和array.cpp(main.cpp保持不变)。

// array.h? ? ? ?
template
class array
{
? ? ? T data_[SIZE];
? ? ? array (const array& other);
? ? ? const array& operator = (const array& other);
? public:
? ? ? array(){};
? ? ? T& operator[](int i);
? ? ? const T& get_elem (int i) const;
? ? ? void set_elem(int i, const T& value);
? ? ? operator T*();? ? ?
};? ? ? ?


// array.cpp
#include "array.h"

template
? ? ? ?T& array ::operator [](int i)
? ? {
? ? return data_[i];
? ? }

template
? ? ? ?const T& array ::get_elem(int i) const
? ? {
? ? return data_[i];
? ? }

template
? ? ? ?void array ::set_elem(int i, const T& value)
? ? {
? ? data_[i] = value;
? ? }
template array ::operator T*()
? ? {
? ? return data_;
}


如果将这个代码进行编译,就会得到3个连接错误。我们的问题是:

1.首先,为什么会报这些错误?
2.为什么只有3个连接错误?在array.cpp文件中,我们有4个成员函数。


要回答这两个问题,我们需要进一步了解一些关于模版实例化过程的细节问题。

模版实例化
程序员在使用模版类的时候,一个较为常见的错误就是将模版类当作类型(type)来使用。“ 参数化的类型”这个术语常常被用于模版类,这也就是误导程序员将模版类当作类型的一个原因。模版类并不是类型,正如它的名字所描述的,它是模版。下面有几个帮助我们理解模版和类型之间关系的重要概念:

1.编译器通过替换模版参数,用模版来构造类型。这一过程称为实例化(instantiation)。
2.由模版产生的一个类型被称为一个特化(specialization)。
3.模版的实例化只在需要的时候进行(on-demand)。也就是说,编译器在发现代码中使用倒了模版的时候,才产生相应的特化(这个位置被称为实例化点(point of instantiation))。
4.为了创建一个特化,编译器在实例化时不仅需要能够“看到”模版的声明,还要知道模版的定义。 5.模版的实例化是“懒惰”(lazy)的,这就意味着只有使用到的函数的定义才会被实例化。


让我们回过头来看看前面的例子,这个例子里面,array是一个模版,array 是这个模版的一个特化——即一个类型。从array构造array 的这个过程就是实例化。这个实例化点在文件main.cpp里。如果我们用“传统”的方式来组织代码,编译器将会看到这个模版的声明(array.h),而看不到这个模版的定义(array.cpp)。因此,编译器将无法生成array 这个类型。然而,这个时候编译器不会报错,它将假设在其他编译单元中定义了这个类型,所以,就将这个类型名称交给连接器来解析。

那么,另一个编译单元(array.cpp)发生了什么呢?编译器将会对这个模版的定义进行语法分析,并检查语法的正确性,但是不会为成员函数生成代码。为什么会这样呢?因为编译器需要知道模版参数才能够为它生成代码——它需要的是一个类型,而不是一个模版。

所以,连接器在main.cpp和array.cpp两个编译单元中都找不到array 的定义,它就对所有未解析的成员函数报错。

好了,上面这些就回答了我们前面的第一个问题。那么第二个问题呢?我们在array.cpp中定义了4个成员函数,而连接器只报了3个错误信息。这个问题的答案在于 “懒惰的实例化”(Lazy Instantiation)。在main.cpp中,我们并没有使用operator[]这个成员函数,因此,编译器根本不尝试将该函数实例化。

解决方法
既然我们已经知道了问题所在,那么最好能找到一些解决方法。下面列出的就是几种可行的解决方案:

1.让编译器在实例化点处可以“看见”模版的定义。
2.在一个单独的编译单元中,将你需要的类型显示地实例化,这样连接器就能够找到这个类型。
3.使用关键字export。


前面两种方法通常称为 “包含模型”(inclusion model),第三种有时被称为 “分离模型”(separation model)

第一种方式实际上意味着在每个使用该模版的编译单元中,我们不仅仅要包含模版的声明,还要包含相应的定义。在前面的例子中,也就意味着,我们要么采用将所有成员函数作为内联(inlined)函数的第一个array.h版本,要么就是在main.cpp中包含array.cpp文件。这样编译器就能够看见array中所有成员函数的声明和定义,它就能够将array 实例化。这种方法的缺点就是我们的编译单元会变得非常巨大,并且这样会明显增加编译和连接的时间。

下面讨论第二种解决方案。我们可以将我们需要使用的类型显示地实例化。一种比较好的方式就是将所有的显示实例化的代码都放在一个单独的编译单元中。在我们前面的那个例子中,我们可以增加一个templateinstantiations.cpp文件。

// templateinstantiations.cpp
#include "array.cpp"

template class array ; // explicit instantiation


这样,会在templateinstantiations.cpp中生成array 这个类型,而不会在main.cpp中产生,并且连接器会找到相应的定义。如果采用这种方法,我们就不会得到巨大的头部,这样也能够减少编译和连接的时间。同时,头文件也会变得“干净”并且更为可读。然而,在这里我们就不能得到“懒惰实例化”的好处了(显示实例化将会对每个成员函数生成代码),并且,对于较大的项目而言,维护templateinstantiations.cpp也会变得比较繁琐。

第三种解决方案是在模版定义的时候使用export关键字,而剩下的事交给编译器去处理。当我在Stroustrup的书中读到关键字export这个部分的时候,我感到十分欣喜。结果在几分钟后发现在VC 6.0中并没有实现这个关键字,而且再花了一些时间发现根本没有编译器实现了这个关键字(第一个支持这个关键字的编译器在2002年后半年才刚刚发布)。从那时起,我看了许多关于export的资料,发现这个关键字几乎能够解决所有在包含模型中遇到的问题。如果想要关于这个关键字的信息,我推荐可以阅读Herb Sutter的文章。

结论
为了能够开发模版库,我们应该理解模版类并不是“普通类型”,因此,我们在采用模版的时候,应该用不同的角度来思考。本文的目的并不是为了吓退想要做一些模版程序设计的开发人员。恰恰相反的是,我希望这篇文章能够帮助那些刚才是从事模版开发的程序员,让他们能够避免一些常见的错误。

文献
1.Bjarne Stroustrup: "The C++ Programming Language", Addison-Wesley Pub Co; ISBN: 0201889544 ; 3rd edition (June 20, 1997)
2.David Vandevoorde, Nicolai M. Josuttis: "C++ Templates: The Complete Guide", Addison Wesley Professional; ISBN: 0201734842 ; 1st edition (November 12, 2002)

你可能感兴趣的:([翻译]如何组织使用模版技术的源代码)