Stack
...
// 我们还必须对成员函数的声明进行相应的修改,如push函数的实现如下:
template <typename T,
template <typename, typename> // 由于在这里我们并不会用到“模板的模板参数”的模板参数(即上面的ELEM),所以你可以把该名称省略不写
class CONT >
void Stack<T, CONT>::push (T const& elem)
{
elems.push_back(elem); // 附加传入元素的拷贝
}
1. 上面作为模板参数里面的class 不能用typename代替(这里CONT是为了定义一个类,因此只能使用关键字class);
2. 还有一个要知道:函数模板并不支持模板的模板参数;
3. 之所以需要定义“ALLOC”,是因为模板的模板实参“std::deque”具有一个缺省模板参数,为了精确匹配模板的模板参数;
4. 在使用时,第2个参数必须是一个类模板,并且由第一个模板参数T传递进来的类型进行实例化:CONT<T> elems; 一般地,你可以使用类模板内部的任何类型来实例化模板的模板参数;
5.5 零初始化
对于int、double或者指针等基本类型,并不存在“用一个有用的缺省值来对它们进行初始化”的缺省构造函数;相反,任何未被初始化的局部变量都具有一个不确定值。如果我们希望我们的模板类型的变量都已经用缺省值初始化完毕,那么针对内建类型,我们需要做一些处理,如下:
// 函数模板
template <typename T>
void foo()
T x = T(); // 如果T是内建类型,x是0或者false
// 类模板:初始化列表来初始化模板成员
template <typename T>
class MyClass
private:
public:
MyClass() : x() {} // 确认x已被初始化,内建类型对象也是如此
5.6 使用字符串作为函数模板的实参
有时,把字符串传递给函数模板的引用参数会导致出人意料的运行结果:
#include <string>
// 注意,method1:引用参数
template <typename T>
inline T const& max(T const& a, T const& b)
return a < b ? b : a;
// method2:非引用参数
template <typename T>
inline T max2(T a, T b)
return a < b ? b : a;
int main()
std::string s;
// 引用参数
::max("apple", "peach"); // OK, 相同类型的实参
::max("apple", "tomato"); // ERROR, 不同类型的实参
::max("apple", s); // ERROR, 不同类型的实参
// 非引用参数
::max2("apple", "peach"); // OK, 相同类型的实参
::max2("apple", "tomato"); // OK, 退化(decay)为相同类型的实参
::max2("apple", s); // ERROR, 不同类型的实参
上面method1的问题在于:由于长度的区别,这些字符串属于不同的数值类型。也就是说,“apple”和“peach”具有相同的类型char const[6];然而“tomato”的类型则是char const[7]。
method2调用正确的原因是:对于非引用类型的参数,在实参演绎的过程中,会出现数组到指针的类型转换(这种转型通常也被称为decay)。
小结:
如果你遇到一个关于字符数组和字符指针之间不匹配的问题,你会意外地发现和这个问题会有一定的相似之处。这个问题并没有通用的解决方法,根据不同情况,你可以:
1. 使用非引用参数,取代引用参数(然而,这可能会导致无用的拷贝);
2. 进行重载,编写接收引用参数和非引用参数的两个重载函数(然而,这可能会导致二义性);
3. 对具体类型进行重载(譬如对std::string进行重载);
4. 重载数组类型,譬如:
5. 强制要求应用程序程序员使用显式类型转换。
对于我们的例子,最好的方法是为字符串重载max()。无论如何,为字符串提供重载都是必要的,否则比较的将是两个字符串的地址。
------------------------------------------------------------------------------------------------------------
第6章 模板实战
6.1 包含模型
6.1.1 连接器错误
大多数C和C++程序员会这样组织他们的非模板代码:
1. 类(class)和其他类型(other type)都被放在一个头文件中。通常而言,头文件是一个扩展名为.hpp(或者.H, .h, .hh, hxx)的文件;
2. 对于全局变量和(非内联)函数,只有声明放在头文件中,定义则位于dot-C文件。通常而言,dot-C文件是指扩展名为.cpp(或者.C, .c, .cc, .cxx)的文件。
这样一切都可以正常运作了。所需的类型定义在整个程序中都是可见的;并且对于变量和函数而言,链接器也不会给出重复定义的错误。
但这种情况在模板中会出现一些问题,如下:
// -----------------------------------------------------------
//basics/myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// 模板声明
template <typename T>
void print_typeof(T const&)
#endif // MYFIRST_HPP
// -----------------------------------------------------------
//basics/myfirst.cpp
// 提供了“应该实例化哪个模板定义”,但没有提供“要基于哪个模板实参来进行实例化”的说明,因为模板实参只存在myfirshmain.cpp中;
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// 模板的实现/定义
template <typename T>
void print_typeof(T const& x)
std::cout << typeid(x).name() << std::endl;
// -----------------------------------------------------------
//basics/myfirstmain.cpp
// 提供了“要基于哪个模板实参来进行实例化”,但没有提供“应该实例化哪个模板定义”的说明,因为模板定义只存在于myfirst.cpp中;
#include "myfirst.hpp" // 按照编程习惯,我们一般都是包含头文件,这个时候会产生链接错误;下面会解析为何为报错;
// #include "myfirst.cpp" // 如果包含cpp文件,程序可以正常编译链接,因为myfirst.cpp有模板定义;
// 使用模板
int main()
double ice = 3.0;
print_typeof(ice); // 调用参数类型为double的函数模板
大多数C++编译器都会顺利地接受这个程序;但是链接器可能会报错,提示找不到函数print_typeof()的定义。
事实上,这个错误的原因在于:
函数模板print_typeof()的定义还没有被实例化。为了使模板真正得到实例化,编译器必须知道:应该实例化哪个定义以及要基于哪个模板实参来进行实例化。遗憾的是,在前面的例子里,这两部分信息位于分开编译的不同文件里面。
因此,当我们的编译器在myfirstmain.cpp中看到print_typeof()调用,但还没有看到(基于double实例化的)函数定义的时候(在这个时候,或者说在这个条件下),它只能假设在别处提供了这个定义(但它不知道是哪里提供了),并产生一个指向该定义的引用(这个引用是用来指向该定义的,只不过它目前无法确定,或者说还没有给这个引用赋值,只能让链接器利用该引用来解决这个问题)。
另一方面,当编译器处理文件myfirst.cpp的时候,它并没有指出:编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化;(个人理解:前面编译器把一个引用提供给了链接器,希望链接器能解决“找不到函数定义”的问题。如果对于普通函数,那么当编译器处理文件myfirst.cpp的时候,是可以确定函数定义的,虽然这个函数定义产生于另一个翻译单元,但可被链接器找到;但这里,我们在myfirst.cpp中定义的是一个函数模板,并且也没有指出“编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化”(没有显示实例化地指出应该根据哪个实参进行实例化),这样的话,函数模板的定义依然是不确定的,链接器在此时便报了找不到函数定义的错误)。
要解决上面的问题,可以从两个点入手:
(1)解决找不到函数模板定义问题(包含模型);
(2)解决没有指出“编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化”问题(显示实例化)。
6.1.2 头文件中的模板
对于前面的问题,我们通常是采取对待宏或内联函数的解决方法:我们把模板的定义也包含在声明模板的头文件里面,即让定义和声明都位于同一个头文件中。我们称模板的这种组织方式为包含模型。针对包含模型的组织方式,我们可以得出:包含模型明显增加了包含头文件myfirst.hpp的开销。
从包含模型得出的另一个结论是:非内联函数模板与“内联函数和宏”有一个很重要的区别,那就是非内联函数模板在调用的位置并不会被扩展,而是当它们基于某种类型进行实例化之后,才产生一份新的(基于该类型的)函数拷贝(所以对于非内联函数模板而言,实例化之后才能确定为一个针对特定类型的函数)。
最后,我们需要指出的是:在我们的例子中应用到普通函数模板的所有特性,对类模板的成员函数和静态数据成员、成员函数模板也都是适用的。
6.2 显式实例化
包含模型能够确保所有需要的模板都已经实例化。这是因为:当需要进行实例化的时候,C++编译系统会自动产生所对应的实例化体。另外,C++标准还提供了一种手工实例化模板的机制:显式实例化指示符。
6.2.1 显式实例化的例子
为了说明手工实例化,让我们回顾前面那个导致链接器错误的例子。在此,为了避免这个链接期错误,我们可以通过给程序添加下面的文件:
//basics/myfirstinst.cpp
#include "myfirst.cpp"
// 基于类型double显式实例化print_typeof()
template void print_typeof<double>(double const&);
显式实例化指示符由关键字template和紧接其后的我们所需要实例化的实体(可以是类、函数、成员函数等)的声明组成,而且,该声明是一个已经用实参完全(注意,是完全)替换参数之后的声明。该指示符也适用于成员函数和静态数据成员,如:
// 基于int显式实例化MyClass<>的构造函数
template MyClass<int>::MyClass();
// 基于int显式实例化函数模板max()
template int const& max(int const&, int const&);
你还可以显式实例化类模板,这样就可以同时实例化它的所有类成员。但有一点需要注意:对于那些在前面已经实例化过的成员,就不能再次对它们进行实例化(针对每个不同实体,不能存在多个显式实例化体,同时显式实例化体和模板特化也只能二者选其一)。
// 基于int显式实例化类Stack<>
template class Stack<int> // 实例化它的所有类成员
// 错误,对于int,不能再次对它进行显式实例化
template Stack<int>:::Stack();
// 基于string显式实例化Stack<>的某些成员函数
template Stack<std::string>::Stack();
template void Stack<std::string>::push(std::string const&);
template std::string Stack<std::string>::top() const;
注意:人工实例化有一个显著的缺点:我们必须仔细跟踪每个需要实例化的实体。对于大项目而言,这种跟踪会带来巨大负担,因此,我们并不建议使用这种方法。其优点在于,显式实例化可以精确控制模板实例的准确位置。
6.2.2 整合包含模型和显式实例化
将模板的定义和模板的声明放在两个不同的文件中。通常的做法是使用头文件来表示这两个文件(xxx.hpp,xxxdef.hpp)。如下:
// stack.hpp
#ifndef STACK_HPP
#define STACK_HPP
#include <vector>
template <typename T>
class Stack
private:
std::vector<T> elems;
public:
Stack();
void push(T const&);
void pop();
T top() const;
#endif
// stackdef.hpp
#ifndef STACKDEF_HPP
#define STACKDEF_HPP
#include "stack.hpp"
template <typename T>
void Stack<T>::push(T const& elem)
elems.push_back(elem);
#endif
// stacktest1.cpp
// 注意,这里和前面链接器报错的例子不同,这里是包含进了stackdef.hpp,
// 这个文件里面含有函数的定义,所以不会产生链接器找不到的错误(其实在编译器中就已经能找到函数模板的定义了)
#include "stackdef.hpp" // 书中是“stack.hpp”,应该有误
#include <iostream>
#include <string>
int main()
Stack<int> intStack;
intStack.push(42);
// stack_inst.cpp
#include "stack.hpp" // 书中是“stackdef.hpp”,应该有误
#include <string>
template Stack<int>;
template Stack<std::string>::Stack();
template void Stack<std::string>::push(std::string const&);
template std::string Stack<std::string>::top() const;
6.3 分离模型
上面给出的两种方法(包含模型和显式实例化)都可以正常工作,也完全符合C++标准。然而,标准还给出了另一种机制:导出模板。这种机制通常也被称为C++模板的分离模型。
6.3.1 关键字 export
大体上讲,关键字export的功能使用是非常简单的:在一个文件里面定义模板,并在模板的定义和(非定义的)声明的前面加上关键字export。对于上面的例子改写如下:
// basics/myfirst3.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// 模板声明
export
template <typename T>
void print_typeof(T const&);
#endif // MYFIRST_HPP
1. 即使在模板定义不可见的条件下,被导出的模板也可以正常使用。换句话说,使用模板的位置和模板定义的位置可以在两个不同的翻译单元。
2. 在一个预处理文件内部(就是指在一个翻译单元内部),我们只需要在第一个声明前面标记export关键字就可以了,后面的重新声明(也包括定义)会隐式地保留这个export特性,这也是我们不需要修改文件myfirst.cpp的原因所在。另一方面,在模板定义中提供一个冗余的export关键字也是可取的,因为这样可以提高代码的可读性。
3. 实际上关键字export可以应用于函数模板、类模板的成员函数、成员函数模板和类模板的静态数据成员。
4. 另外,它还可以用于类模板的声明,这将意味着每个可导出的类成员(注意,是可导出的类成员,不可导出的依然不可导出)都被看做可导出实体,但类模板本身实际上却没有被导出(因此,类模板的定义仍然需要出现在头文件中)。你仍然可以隐式或者显式地定义内联成员函数。然而,内联函数却是不可导出的,如下:
export template <typename T>
class MyClass
public:
void memfun1(); // 被导出的函数
void memfun2(){ ... } // 隐式内联不能被导出
void memfun3(); // 显式内联不能被导出
template <typename T>
inline void MyClass<T>::memfun3() // 使用inline关键字,显式内联
5. export 关键字不能和inline关键字一起使用;
6. 如果用于模板的话,export要位于关键字template的前面,如下:
tempalte <typename T>
class Invalid
public:
export void wrong(T); // ERROR, export 没有位于template之前
export template <typename T> // ERROR,同时使用export和inline
inline void Invalid<T>::wrong(T) { ... }
6.3.2 分离模型的限制
1. export特性为能像其他C++特性那样广为流传;
2. export需要系统内部为“模板被实例化的位置和模板定义出现的位置”建立一些我们看不见的耦合;
3. 被导出的模板可能会导致出人意料的语义[?TODO]。
6.3.3 为分离模型做好准备 一个好的办法就是:对于我们预先编写的代码,存在一个可以包含模型和分离模型之间互相切换的开关。我们使用预处理指示符来获得这种特性,如下:
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// 如果定义了USE_EXPORT,就使用export
#if defined(USE_EXPORT)
#define EXPORT export
#else
#define EXPORT
#endif
// 模板声明
EXPORT
template <typename T>
void print_typeof(T const&);
// 如果没有定义USE_EXPORT,就包含模板定义
#if !defined(USE_EXPORT)
#include "myfirst.cpp"
#endif
#endif // MYFIRST_HPP
6.4 模板和内联
把短小函数声明为内联函数是提高运行效率所普遍采用的方法。inline修饰符表明的是一种实现:在函数的调用处使用函数体(即内容)直接进行内联替换,它的效率要优于普通函数的调用机制(针对短小函数而言)。然而,标准并没有强制编译器实现这种“在调用处执行内联替换”的机制,实际上,编译器也会根据调用的上下文来决定是否进行替换(内联并不是一种强制执行的机制)。
函数模板和内联函数都可以被定义于多个翻译单元中。通常,我们是通过下面的途径来获取这个实现:把定义放在一个头文件中,而这个头文件又被多个dot-C文件所包含(#include)。
这种实现会给我们这样一个印象:函数模板缺省情况下是内联的。然而,这种想法是不正确的。所以,如果你编写需要被实现为内联函数的函数模板,你仍然应该使用inline修饰符(除非这个函数由于是在类定义的内部进行定义的而已经被隐式内联了)。
因此,对于许多不属于类定义一部分的短小模板函数,你应该使用关键字inline来声明它们。
6.5 预编译头文件
1. 当翻译一个文件时,编译器是从文件的开头一直进行到文件的末端的;
2. 当处理文件中的每个标记(这些标记可能来自于#include的文件)时,编译器会匹配它的内部状态,包括添加入口点到符合表,从而在后面可以查找等。在这个过程中,编译器还会在目标文件中生成代码。
3. 预编译头文件机制主要依赖于下面的事实:我们可以使用某种方式来组织代码,让多个文件中前面的代码都是相同的。充分利用预处理头文件的关键之处在于:(尽可能地)确认许多文件开始处的相同代码的最大行数。这意味着以#include指示符开始,同时意味着包含顺序也相当重要;
4. 通常我们会直接创建一个名为std.hpp的头文件,让它包含所有的标准头文件;
5. 管理预编译头文件的一种可取方法是:对预编译头文件进行分层,即根据头文件的使用频率和稳定性来进行分层。
6.6 调试模板
我们叙述的大多数编译期错误就是由于违反了某些约束而产生的,我们把这些约束称为语法约束;而对于其他约束,我们称为语义约束。concept这个术语通常被用于表示:在模板库中重复需求的约束集合。concept还可以形成体系:就是说,某个concept可以是其他concept的进一步细化(也称为精华,更严格的约束),更精华的concept不但具备上层concept的各种约束,而且还增加了一些针对自身的约束。调试模板代码的主要工作是判断模板实现和模板定义中哪些concept被违反了。
更详细的内容参见书籍。
------------------------------------------------------------------------------------------------------------
第7章 模板术语
7.1 “类模板”还是“模板类”
在C++中,类和联合(union)都被称为类类型(class type)。如果不加额外的限定,我们通常所说的“类(class)”是指:用关键字class或者struct引入的类类型。
需要特别注意的一点就是:类类型包括联合,而“类”不包括联合。
7.2 实例化和特化
模板实例化是一个通过使用具体值替换模板实参,从模板产生出普通类、函数或者成员函数的过程。这个过程最后获得的实体(譬如类、函数或成员函数)就是我们通常所说的特化。
在C++中,实例化过程并不是产生特化的唯一方式。程序员可以使用其他机制来显式地指定某个声明,该声明对模板参数进行特定的替换,从而产生特化,如:
1. 声明是一种C++构造,它引入(或重新引入)一个名称到某个C++作用域(scope)中;
2. 另外,对于宏定义和goto语句而言,即使它们都具有一个名称,但它们却不属于声明的范畴;
3. 如果已经确定了这种C++构造(即声明)的细节,或者对于变量而言,已经为它分配了内存空间,那么声明就变成了定义;
4. 对于“类类型或者函数”的定义,这意味着必须提供一对花括号内部的实体;
5. 对于变量而言,进行初始化和不具有extern关键字的声明都是定义。编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化
7.4 一处定义原则(ODR)
“C++语言的定义”在各个实体的重新声明上面强加了一些约束,一处定义原则(或称为ODR,one-definition rule)就是这些约束的全体。基本原则如下:
1. 和全局变量与静态数据成员一样,在整个程序中,非内联函数和成员函数只能被定义一次;
2. 类类型和内联函数在每个翻译单元中最多只能被定义一次,如果存在多个翻译单元,则其所有的定义都必须是等同的。
7.5 模板实参和模板参数
模板参数:位于模板声明或定义内部,关键字template后面所列举的名称;
模板实参:用来替换模板参数的各个对象。
一个基本原则是:模板实参必须是一个可以在编译期确定的模板实体或者值。如下:
template <typename T> // 模板参数
class Dozen
public:
ArrayInClass<T, 12> contents; // 模板实参