C++ template —— 模板基础(一)
《C++ Template》对Template各个方面进行了较为深度详细的解析,故而本系列博客按书本的各章顺序编排,并只作为简单的读书笔记,详细讲解请购买原版书籍(绝对物超所值)。
------------------------------------------------------------------------------------------------------------
第一章 前言
1.4 编程风格
(1)对“常整数”趋向使用“int const”,而不是使用“const int”。“恒定不变部分”指的是const限定符前面的部分。
------------------------------------------------------------------------------------------------------------
第1部分 基础
第2章 函数模板
2.1 初探函数模板
2.1.1 定义模板
template <typename T> inline T const& max(T const& a, T const& b) { return a < b ? : b a; }
注:你可以使用任何类型(基本类型、类等)来实例化该类型参数,只要所用类型提供模板使用的操作就可以。
2.1.2 使用模板
max(32, 43);
这种用具体类型代替模板参数的过程叫做实例化,它产生了一个模板的实例。
注:通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生一个不同的实体。
于是,我们可以得出一个结论:模板被编译了两次,分别发生在:
(1)实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。
(2)在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用(该类型没有提供模板所需要使用到的操作)等。
2.2 实参的演绎(deduction)
注:模板实参不允许进行自动类型转换;每个T都必须正确地匹配。如:
max(4, 4.3); // Error:第1个参数类型是int,第2个参数类型是double
2.3 模板参数
函数模板有两种类型的参数(牢记):
(1)模板参数:位于函数模板名称的前面,在一对尖括号内部进行声明:
template <typename T> // T是模板参数
(2)调用参数:位于函数模板名称之后,在一对圆括号内部进行声明:
...max(T const& a, T const& b); // a和b都是调用参数
注:由于函数模板历史发展过程中的一个失误,导致目前(2016/1/11)我们不能在函数模板内部指定缺省的模板实参(形如“template <typename T = xxx>”,不能指定xxx;)。但依然可以指定函数模板的调用实参(形如“...max(T const& a, T const& b = yyy)”,可以指定yyy)(以后应该会支持函数模板指定缺省模板实参)。
注:切记“模板参数”和“模板实参”的区别;函数的“模板参数”和“调用参数”、“模板实参”和“调用实参”的区别;“类型参数”和“非类型参数”的区别[参见下面第4章内容];
函数模板和类模板区别【重要】:
- 函数模板可以进行模板实参演绎(不能演绎返回类型)、重载、指定缺省调用实参、全局特化;不能指定缺省模板实参,不能局部特化;
- 类模板可以指定缺省模板实参、指定全局特化和局部特化(用来完成类似函数模板重载功能);不能重载类模板,不能进行实参演绎。
(3)显式实例化:当模板参数和调用参数没有发生关联,或者不能由调用参数来决定模板参数的时候(例如:返回类型),在调用时就必须显式指定模板实参。切记,模板实参演绎并不适合返回类型。如下:
template <typename T1, typename T2, typename RT> inline RT max(T1 const& a, T2 const& b);
那么必须进行显式实例化:
max<int, double, double>(4, 4.3); // OK,但很麻烦。这里T1和T2是不同的类型,所以可以指定两个不同类型的实参4和4.3
注:通常而言,你必须指定“最后一个不能被隐式演绎的模板实参之前的”所有实参类型。上面的例子中,改变模板参数的声明顺序,那么调用者就只需要指定返回类型:
template <typename RT, typename T1, typename T2> inline RT max(T1 const& a, T2 const& b); ... max<double>(4, 4.3); // ok,返回类型是double
2.4 重载函数模板
- 注:对于非模板函数和同名的函数模板,如果其他条件都是相同的话,那么在调用的时候,重载解析过程通常会优先调用非模板函数,而不会从该模板产生出一个实例。然而,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
- 注:可以显式地指定一个空的模板实参列表,这个语法好像是告诉编译器:只有模板才能匹配这个调用(即便非模板函数更符合匹配条件也不会被调用到),而且所有的模板参数都应该根据调用实参演绎出来。
max<>(7, 42); // call max<int>(通过实参演绎)
- 注:因为模板是不允许自动类型转化的;但普通函数可以进行自动类型转换,所以当一个匹配既没有非模板函数,也没有函数模板可以匹配到的时候,会尝试通过自动类型转换调用到非模板函数(前提是可以转换为非模板函数的参数类型)。
- 注:在所有重载的实现里面,我们都是通过引用来传递每个实参的。一般而言,在重载函数模板的时候,最好只是改变那些需要改变的内容;就是说,你应该把你的改变限制在下面两种情况:改变参数的数目或者显式地指定模板参数。否则可能会出现非预期的结果。
(需要参考书本2.4节p17,p18例子,重点研究如下的传参方式):
// 求两个指针所指向值的最大者 // T* const& a :表示 T类型的指针常量a(a指针的指向不可修改),并且指针常量a是通过引用方式传参;
// 如果修改为 T* const a :表示 T类型的指针常量a,并且指针常量a通过传值的方式传参,那么会在内存栈中创建指针常量a的一个临时副本(假设叫做pa),pa和a都指向同一个内存地址; // 甚至可修改为 T*& a : 表示 T类型的指针变量a(a指针的指向可以修改),并且指针变量a通过引用方式传参;
template <typename T> inline T* const& max(T* const& a, T* const& b) { return *a < *b ? b : a; }
- 注:一定要让函数模板的所有重载版本的声明都位于它们被调用的位置之前。例如,定义一个重载函数A,而在A1(函数A的重载)中调用A,但是,如果直到A1的定义处还没有见到A的定义(也即函数A的定义在函数A1的后面,但函数A1中调用了函数A),那么并不会调用到这个重载函数A,而会寻找在函数A1之前已经定义了的符合条件的其他函数Ax(即便A是符合条件的非模板函数,而Ax是模板函数,也会由于A的声明太迟,而选择调用Ax)。
------------------------------------------------------------------------------------------------------------
第3章 类模板
3.1 类模板Stack的实现
见书源码;
3.1.1 类模板的声明
template <typename T> //可以使用class代替typename class Stack { ... };
注:这个类的类型是Stack<T>,其中T是模板参数。因此,当在声明中需要使用该类的类型时,你必须使用Stack<T>。然而,当使用类名而不是类的类型时,就应该只用Stack;譬如,当你指定类的名称、类的构造函数、析构函数时,就应该使用Stack。
3.1.2 成员函数的实现
为了定义类模板的成员函数,你必须指定该成员函数是一个函数模板,而且你还需要使用这个类模板的完整类型限定符。如下:
template <typename T> void Stack<T>::push(T const& elem) { elems.push_back(elem); }
显然,对于类模板的任何成员函数,你都可以把它实现为内联函数,将它定义于类声明里面,如:
template <typename T> class Stack { ... void push(T const& elem) // 隐式内联 { elems.push_back(elem); } ... };
3.2 类模板Stack的使用
为了使用类模板对象,必须显式地指定模板实参。
Stack<in> intStack; // 必须显式指定模板实参int
- 1. 只有那些被调用的成员函数,才会产生这些函数的实例化代码。对于类模板,成员函数只有在被使用的时候才会被实例化。显然,这样可以节省空间和时间;
- 2. 另一个好处是,对于那些“未能提供所有成员函数中所有操作的”类型,你也可以使用该类型来实例化类模板,只要对那些“未能提供某些操作的”成员函数,模板内部不使用就可以。
- 3. 如果类模板中含有静态成员,那么用来实例化的每种类型,都会实例化这些静态成员。
- 切记,要作为模板参数类型,唯一的要求就是:该类型必须提供被调用的所有操作。
3.3 类模板的特化
语法:为了特化一个类模板,你必须在起始处声明一个template<>,接下来声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:
template<> class Stack<std::string> { ... };
进行类模板的特化时,每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代。如:
void Stack<std::string>::push(std::string const& elem) { elems.push_back(elem); }
3.4 局部特化(偏特化)
例子如下:
// 两个模板参数具有相同的类型 template <typename T> class Myclass<T, T> // { }; // 第2个模板参数的类型是int template <typename T> class Myclass<T, int> { }; // 也是一种模板特化,两个模板参数都是指针类型 template <typename T1, typename T2> class Myclass<T1*, T2*> // 也可以使引用类型T&,常引用等 { };
3.5 缺省模板实参
对于类模板,你还可以为模板参数定义缺省值;这些值就被称为缺省模板实参;而且它们还可以引用之前的模板参数。(STL容器使用缺省默认实参指定内存分配其alloc)如:
template <typename T, typename CONT = std::vector<T> > class Stack { };
------------------------------------------------------------------------------------------------------------
第4章 非类型模板参数
4.1 非类型的类模板参数
如下定义:
// 非类型的类模板参数 int MAXSIZE :这个模板参数接受的不是一个类型,而是一个值(非类型)
template <typename T, int MAXSIZE> class Stack { }; // 使用,第2个模板实参传入的不是类型,而是一个具体的值 Stack<int, 20> int20Stack; // 可以存储20个int元素的栈 Stack<int, 40> int40Stack; // 可以存储40个int元素的栈
注:每个模板实例都具有自己的类型,因此int20Stack和int40Stack属于不同的类型,而且这两种类型之间也不存在显式或者隐式的类型转换;所以它们之间不能互相替换,更不能互相赋值。
然而,如果从优化设计的观点来看,这个例子并不适合使用缺省值。缺省值应该是直观上正确的值。但对于栈的类型和大小而言,int类型和最大容量100从直观上看起来都不是正确的。因此,在这里最好还是让程序员显式地指定这两个值。因此我们可以在设计文档中用一条声明来说明这两个属性(即类型和最大容量)。
4.2 非类型的函数模板参数
如下定义:
template <typename T, int VAL> T addValue(T const& x) { return x + VAL; }
借助标准模板库(STL)使用上面例子:
std::transform(source.begin(), source.end(), dest.begin(), addValue<int, 5>);
注:
- 1. 上面的调用中,最后一个实参实例化了函数模板addValue(),它让int元素增加5.
- 2. 这个例子有一个问题:addValue<int, 5>是一个函数模板实例,而函数模板实例通常被看成是用来命名一组重载函数的集合(即使该组只有一个函数)。然而,根据现今的标准,重载函数的集合并不能被用于模板参数的演绎(注意,标准模板库中的函数是使用模板定义的,故而在transform()函数中,参数是作为函数模板调用实参传递的,也即参与了模板参数演绎)。于是,你必须将这个函数模板的实参强制类型转换为具体的类型:
std::transform(source.begin(), source.end(), dest.begin(), (int(*)(int const&))addValue<int, 5>);
4.3 非类型模板参数的限制
我们还应该知道:非类型模板参数是有限制的。通常而言,它们可以是常整数(包括枚举值)或者指向外部链接对象的指针,你不能使用浮点数、class类型的对象和内部链接对象(例如string)作为实参;
- 名称链接性的介绍参见:存储持续性、作用域和链接性
- 参见:8.2 模板参数_8.2.2 非类型参数
注:
- 之所以不能使用浮点数(包括简单的常量浮点表达式)作为模板实参是有历史原因的。然而以后可能会支持这个特性。
- 另外,由于字符串文字是内部链接对象(因为两个具有相同名称但出于不同模块的字符串,是两个完全不同的对象),所以你不能使用它们来作为模板实参,如下:
template <char const* name> class MyClass { }; MyClass<"hello"> x; // ERROR:不允许使用字符文字"hello"
////////////////////////////////////////////////////////// //另外,你也不能使用全局指针作为模板参数: template <char const* name> class MyClass { ... }; char const* s = "hello"; MyClass<s> x; // ERROR:s是一个指向内部链接对象的指针
/////////////////////////////////////////////////////////// //然而,你可以这样使用: template <char const* name> class MyClass { ... }; extern char const s[] = "hello"; MyClass<s> x; // OK //全局字符数组s由“hello”初始化,是一个外部链接对象。
------------------------------------------------------------------------------------------------------------
第5章 技巧性基础知识
5.1 关键字typename
在C++标准化过程中,引入关键字typename是为了说明:模板内部的标识符可以是一个类型。如下:
template <typename T> class MyClass { // 这里的typename被用来说明:T::SubType是定义于类T内部的一种类型 typename T::SubType* ptr; ... };
注:本节同时提到了一个奇特的构造“.template”:只有当该前面存在依赖于模板参数的对象时,我们才需要在模板内部使用“.template”标记(和类似的诸如->template的标记),而且这些标记也只能在模板中才能使用。如下例子:
void printBitset(std::bitset<N> const& bs) { // 如果没有使用这个template,编译器将不知道下列事实:bs.template后面的小于号(<)并不是数学 // 中的小于号,而是模板实参列表的起始符号。只有在编译器判断小于号之前,存在依赖于模板参数的构造 // 才会出现这种问题。在这个例子中,传入参数bs就是依赖于模板参数N的构造 std::cout << bs.template to_string<char, char_traits<char>, allocator<char> >(); }
5.2 使用this->
考虑例子:
template <typename T> class Base { public: void exit(); }; template <typename T> class Derived : Base<T> // 模板基类 { public: void foo() { exit(); // (1)调用外部的exit()或者出现错误,而不会调用模板基类的exit(),下面解析。 } };
注意:(C++ template 5.2&9.4.2)
- 非依赖型:指并不依赖于模板参数;
- C++标准规定:对于模板中的非依赖型名称,将会在看到的第一时间进行查找;
- 同时C++标准又声明:非依赖型名称不会在依赖型基类中进行查找。原因是,以上面例子解析,依赖型基类Base<T>有可能被特化,并且特化版本有可能不提供一般性template相同的接口(比如exit),这个时候编译器会在(1)处报错;
- 解决方法:我们可以让exit也成为依赖型名称,因为依赖型名称只有在实例化时才会进行查找;而在实例化时,基类的特化是已知的;
- 让非依赖型名称成为依赖型名称,有如下3个方法:
- this->:foo()内,在基类函数调用动作之前加上“this->";[修改调用形式为:this->exit();]
- Base<T> :明白指出被调用函数位于基类;[修改调用形式为:Base<T>::exit();]
- 使用using:如果不想重复使用Base<T>限定名称,可以使用using来告诉编译器被调用函数位于基类;[增加语句:using Base<T>::exit; 之后就可以在foo()内直接调用了]
- 注意:如果原来的非依赖型名称是被用于虚函数调用的话,那么类似Base<T>这种引入依赖性的限定会禁止虚函数调用,可能会导致错误;
- 当形如Base<T>的方式不适用时,可以使用this->。实际上,我们更趋向于在允许使用this->前缀的地方都使用this->前缀,这同样适用于非模板代码;
- 对于模板化基类内的名称处理,可以参见博文Effective C++ —— 模板与泛型编程(七) 条款43 学习处理模板化基类内的名称;
5.3 成员模板
对于类模板而言,其实例化只有在类型完全相同才能相互赋值。我们通过定义一个身为模板的赋值运算符(成员模板),来达到两个不同类型(但类型可以转换)的实例进行相互赋值的目的,如下声明:
template <typename T> class Stack { ...
// 成员函数模板的模板参数T2和Stack模板的模板参数T不同,表示2个不同类型的相互赋值(支持隐式转换)
template <typename T2> Stack<T>& operator= (Stack<T2> const&); };
参见博文Effective C++ —— 模板与泛型编程(七) 条款45 运用成员函数模板接受所有兼容类型
5.4 模板的模板参数
还是以Stack为例:
template <typename T, template <typename ELEM, typename ALLOC = std::allocator<ELEM> > class CONT = std::deque > // 为模板的模板参数提供模板实参缺省值std::deque class 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: T x; 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. 重载数组类型,譬如:
template <typename T, int N, int M> T const* max (T const (&a)[N], T const (&b)[M]) { return a < b ? b : a; }
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++中,实例化过程并不是产生特化的唯一方式。程序员可以使用其他机制来显式地指定某个声明,该声明对模板参数进行特定的替换,从而产生特化,如:
template <typename T1, typename T2> // 基本的类模板 class MyClass { ... };
// 完全特化 template<> // 显式特化 class MyClass<std::string, float> { ... };
严格地说,上面就是我们通常所讲的显式特化(区别于实例化特化或者其他方式产生的特化)。
template <typename T> // 基本的类模板 class MyClass<T, T> { ... }; template<typename T> // 局部特化 class MyClass<bool, T> { ... };
另外,当谈及(显式或隐式)特化的时候,我们把普通模板称为基本模板。
7.3 声明和定义
- 1. 声明是一种C++构造,它引入(或重新引入)一个名称到某个C++作用域(scope)中;
- 2. 另外,对于宏定义和goto语句而言,即使它们都具有一个名称,但它们却不属于声明的范畴;
- 3. 如果已经确定了这种C++构造(即声明)的细节,或者对于变量而言,已经为它分配了内存空间,那么声明就变成了定义;
- 4. 对于“类类型或者函数”的定义,这意味着必须提供一对花括号内部的实体;
- 5. 对于变量而言,进行初始化和不具有extern关键字的声明都是定义。编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化
7.4 一处定义原则(ODR)
“C++语言的定义”在各个实体的重新声明上面强加了一些约束,一处定义原则(或称为ODR,one-definition rule)就是这些约束的全体。基本原则如下:
- 1. 和全局变量与静态数据成员一样,在整个程序中,非内联函数和成员函数只能被定义一次;
- 2. 类类型和内联函数在每个翻译单元中最多只能被定义一次,如果存在多个翻译单元,则其所有的定义都必须是等同的。
注:
- 一个翻译单元是指:预处理一个源文件所获得的结果;就是说,它包括#include指示符(即所包含的头文件)所包含的内容。
- 另外,我们所说的可链接实体指的是下面的实体:非内联函数或者非内联成员函数、全局变量或者静态成员变量,还包括从模板产生的上述这些实体。
7.5 模板实参和模板参数
模板参数:位于模板声明或定义内部,关键字template后面所列举的名称;
模板实参:用来替换模板参数的各个对象。
一个基本原则是:模板实参必须是一个可以在编译期确定的模板实体或者值。如下:
template <typename T> // 模板参数 class Dozen { public: ArrayInClass<T, 12> contents; // 模板实参 };