C++模板的语法
一、函数模板和类模板
函数模板(function template)和类模板(class template)的简单示例如下(参考:传送门):
#include <iostream>
// 函数模板
template<typename T>
bool equivalent(const T& a, const T& b) {
return !(a < b) && !(b < a);
}
// 类模板
template<typename T = int> // 默认参数
class bignumber {
T _v;
public:
bignumber(T a) : _v(a) { }
inline bool operator<(const bignumber& b) const; // 等价于 (const bignumber<T>& b)
};
// 在类模板外实现成员函数
template<typename T>
bool bignumber<T>::operator<(const bignumber& b) const {
return _v < b._v;
}
int main()
{
bignumber<> a(1), b(1); // 使用默认参数,"<>"不能省略
std::cout << equivalent(a, b) << '\n'; // 函数模板参数自动推导
std::cout << equivalent<double>(1, 2) << '\n';
return 0;
}
程序输出如下:
1
0
关于模板(函数模板、类模板)的模板参数:
- 类型参数(type template parameter),用 typename 或 class 标记;
- 非类型参数(non-type template parameter)可以是:整数及枚举类型、对象或函数的指针、对象或函数的引用、对象的成员指针,非类型参数是模板实例的常量;
- 模板型参数(template template parameter),如“template<typename T, template<typename> class A> someclass {};”;
- 模板参数可以有默认值(函数模板参数默认是从 C++11 开始支持);
- 函数模板的和函数参数类型有关的模板参数可以自动推导,类模板参数不存在推导机制;
- C++11 引入变长模板参数,请见下文。
二、模板特例化
模板特例化(template specialization,又称特例、特化)的简单示例如下:
// 实现一个向量类
// 模板通例(primary template),具体实现 //
template<typename T, int N>
class Vec {
T _v[N];
};
// 对 Vec<float, 4> 进行专门实现,如利用向量指令进行加速 //
template<>
class Vec<float, 4> {
float _v[4];
};
// 对 Vec<bool, N> 进行专门实现,如用一个比特位表示一个bool //
template<int N>
class Vec<bool, N> {
char _v[(N + sizeof(char) - 1) / sizeof(char)];
};
所谓模板特例化即对于通例中的某种或某些情况做单独专门实现(没有通例将报错:主模板的声明中不允许使用模板参数列表)。
最简单的情况是对每个模板参数指定一个具体值,这成为完全特例化(full specialization),另外,可以限制模板参数在一个范围取值或满足一定关系等,这称为部分特例化(partial specialization),用数学上集合的概念,通例模板参数所有可取的值组合构成全集U,完全特例化对U中某个元素进行专门定义,部分特例化对U的某个真子集进行专门定义。
更多模板特例化的例子如下:
template<typename T, int i>
class cp00{ // 用于模板型模板参数
};
template<typename T1, typename T2, int i, template<typename, int> class CP>
class TMP{ // 通例
};
template<>
class TMP<int, float, 2, cp00>{ // 完全特例化
};
template<typename T1, typename T2, int i, template<typename, int> class CP>
class TMP<const T1, T2, i, CP>{ // 第一个参数有const修饰
};
template<typename T, int i>
class TMP<cp00<T, i>, cp00<T, i+10>, i, cp00>{ // 第一二个参数为cp00的实例且满足一定关系,第四个参数为cp00
};
template<template<int i> CP>
class TMP<int, float, 10, CP>{ // 编译错误!,第四个参数类型和通例类型不一致
};
关于模板特例化:
- 在定义模板特例之前必须已经有模板通例(primary template)的声明;
- 模板特例并不要求一定与通例有相同的接口,但为了方便使用(体会特例的语义)一般都相同;
- 匹配规则,在模板实例化时如果有模板通例、特例加起来多个模板版本可以匹配,则依据如下规则:对版本AB,如果 A 的模板参数取值集合是B的真子集,则优先匹配 A,如果 AB 的模板参数取值集合是“交叉”关系(AB 交集不为空,且不为包含关系),则发生编译错误,对于函数模板,用函数重载分辨(overload resolution)规则和上述规则结合并优先匹配非模板函数。
对模板的多个实例,类型等价(type equivalence)判断规则:同一个模板(模板名及其参数类型列表构成的模板签名(template signature)相同,函数模板可以重载,类模板不存在重载)且指定的模板实参等价(类型参数是等价类型,非类型参数值相同)。如下例子:
#include <iostream>
// 识别两个类型是否相同,提前进入模板元编程^_^
template<typename T1, typename T2> // 通例,返回 false
class theSameType {
public:
enum { ret = false };
};
template<typename T> // 特例,两类型相同时返回 true
class theSameType<T, T> {
public:
enum { ret = true };
};
template<typename T, int i>
class aTMP {
};
int main() {
typedef unsigned int uint;
typedef uint uint2;
std::cout << theSameType<unsigned, uint2>::ret << '\n';
//std::cout << theSameType<int, uint2>::ret << '\n'; //此时输出0,应为uint和int不同
std::cout << theSameType<aTMP<unsigned, 2>, aTMP<uint2, 2>>::ret << '\n';
std::cout << theSameType<aTMP<int, 2>, aTMP<int, 3>>::ret << '\n';
return 0;
}
1
1
0
三、模板实例化
关于模板实例化(template instantiation):
- 指在编译或链接时生成函数模板或类模板的具体实例源代码,即用使用模板时的实参类型替换模板类型参数(还有非类型参数和模板型参数);
- 隐式实例化(implicit instantiation):当使用实例化的模板时自动地在当前代码单元之前插入模板的实例化代码,模板的成员函数一直到引用时才被实例化;
- 显式实例化(explicit instantiation):直接声明模板实例化,模板所有成员立即都被实例化;
- 实例化也是一种特例化,被称为实例化的特例(instantiated (or generated) specialization)。
隐式实例化时,成员只有被引用到才会进行实例化,这被称为推迟实例化(lazy instantiation),由此可能带来的问题如下面的例子:
#include <iostream>
template<typename T>
class aTMP {
public:
void f1() {
std::cout << "f1()\n";
}
void f2() {
std::ccccout << "f2()\n";
} // 敲错键盘了,语义错误:没有 std::ccccout
};
int main() {
aTMP<int> a;
a.f1();
// a.f2(); // 这句代码被注释时,aTMP<int>::f2() 不被实例化,从而上面的错误被掩盖!
return 0;
}
输出结果:
f1()
所以模板代码写完后最好写个诸如显示实例化的测试代码,更深入一些,可以插入一些模板调用代码使得编译器及时发现错误,而不至于报出无限长的错误信息。另一个例子如下(GCC 4.8 下编译的输出信息,VS2013 编译输出了 500 多行错误信息):
#include <iostream>
// 计算 N 的阶乘 N!
template<int N>
class aTMP{
public:
enum { ret = N==0 ? 1 : N * aTMP<N-1>::ret }; // Lazy Instantiation,将产生无限递归!
};
int main(){
std::cout << aTMP<10>::ret << '\n';
return 0;
}
输出错误信息:
sh-4.2# g++ -std=c++11 -o main *.cpp
main.cpp:7:28: error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum) instantiating 'class aTMP<-890>'
enum { ret = N==0 ? 1 : N * aTMP<N-1>::ret };
^
main.cpp:7:28: recursively required from 'class aTMP<9>'
main.cpp:7:28: required from 'class aTMP<10>'
main.cpp:11:23: required from here
main.cpp:7:28: error: incomplete type 'aTMP<-890>' used in nested name specifier
上面的错误是因为,当编译 aTMP<N> 时,并不判断 N==0,而仅仅知道其依赖 aTMP<N-1>(lazy instantiation),从而产生无限递归,纠正方法是使用模板特例化,如下:
#include <iostream>
// 计算 N 的阶乘 N!
template<int N>
class aTMP {
public:
enum { ret = N == 0 ? 1 : N * aTMP<N - 1>::ret }; // Lazy Instantiation,将产生无限递归!
};
template<>
class aTMP<0> {
public:
enum { ret = 1 };
};
int main() {
std::cout << aTMP<10>::ret << '\n';
return 0;
}
输出:
3228800
四、模板的编译和链接
关于模板的编译和链接:
- 包含模板编译模式:编译器生成每个编译单元中遇到的所有的模板实例,并存放在相应的目标文件中;链接器合并等价的模板实例,生成可执行文件,要求实例化时模板定义可见,不能使用系统链接器;
- 分离模板编译模式(使用 export 关键字):不重复生成模板实例,编译器设计要求高,可以使用系统链接器;
- 包含编译模式是主流,C++11 已经弃用 export 关键字(对模板引入 extern 新用法),一般将模板的全部实现代码放在同一个头文件中并在用到模板的地方用 #include 包含头文件,以防止出现实例不一致(如下面紧接着例子);
实例化,编译链接的简单例子如下:
// file: print.cpp
#include <iostream>
template<typename T>
void print(T const& m) {
std::cout << "b.cpp: " << m << '\n';
}
void fb() {
print('2'); // print<char>
print(0.1); // print<double>
}
// file: MyClass .cpp
#include <iostream>
template<typename T>
class MyClass {
};
template MyClass<double>::MyClass(); // 显示实例化构造函数 MyClass<double>::MyClass()
template class MyClass<long>; // 显示实例化整个类 MyClass<long>
template<typename T>
void print(T const& m) {
std::cout << "a.cpp: " << m << '\n';
}
void fa() {
print(1); // print<int>,隐式实例化
print(0.1); // print<double>
}
void fb(); // fb() 在 b.cpp 中定义,此处声明
int main() {
fa();
fb();
return 0;
}
编译结果:
上例中,由于 MyClass.cpp 和 print.cpp 中的 print<double> 实例等价(模板实例的二进制代码在编译生成的对象文件 MyClass.obj、print.obj 中),故链接时消除了一个(消除哪个没有规定,上面消除了 print.cpp 中的)。
其他一些解决办法,也可以参考我另一篇文章:传送门
五、template、typename、this 关键字
首先我们了解一些概念(来源):
从属名称(dependent names): 模板(template)内出现的名称, 相依于某个模板(template)参数, 如T t;
嵌套从属名称(nested dependent names):从属名称在class内呈嵌套装, 如T::const_iterator ci;
非从属名称(non-dependent names): 不依赖任何template参数的名称, 如int value;
提问一个问题,以下模板的声明中, class
和 typename
有什么不同?
template<class T> class Test;
template<typename T> class Test;
答案:没有不同。然而,C++ 并不总是把 class 和 typename 视为等价。但是,有时候我们一定得使用 typename。
- 如果不特定指出typename, 嵌套从属名称, 有可能产生解析(parse)歧义.
- 任何时候在模板(template)中指涉一个嵌套从属类型名称, 需要在前一个位置, 添加关键字typename;
- 否则报错(GCC): error: need 'typename' before 'T::xxx' because 'T' is a dependent scope
我们看一个例子(来源):
template
void print(const C & container)
{
C::const_iterator iter(container.begin());
cout << *iter << endl;
int value = *iter;
return;
}
在上述代码中,iter的类型是C::const_iterator,实际的类型取决于C的类型。const_iterator 同时也是C内部的typedef 类型名。 但是,在此处,编译器的行为不会是你预期的。
C++编译器在面对从属名称时,如果此时该从属名称又嵌套了其他类型,如此处的 iter就是C::const_iterator类型,
这里的C::const_iterator 称为嵌套从属类型(嵌套于C类型,从属于模板参数C)。编译器在看到这样的代码时,难免会晕头转向,因为它不知道const_iterator 是C内部定义的类型(后文的traits class你可以看到),还是C内部的成员变量。因此,编译器一致约定说,对于这样的不负责任的输入,编译器一致将其认为 “这不是个类型”!!显然这就需要在代码中明确地告诉它,这是个类型,就这样只需要 在 C::const_iterator 前面加上关键字 typename 即可。
template <class T> //可以是class或者是typename,定义模板
void f(const C &container,typename C::iterator iter); //第一个参数不需要typename,因为它并没
有设计嵌套从属类型,它只是个从属类型(因为与C相关),后面的typename是必须的。
默认情况下,C++ 语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字 typename 来实现这一点:
template<typename T>
typename T::value_type top(const T &c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}
top
函数期待一个容器类型的实参,它使用 typename 指明其返回类型,并在 c
中没有元素时生成一个初始值的元素,并返回给调用者。
测试代码:
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
cout << top<vector<int> >(vec) << endl; // 输出3
在这里我们只需要记住一点,当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename,而不能使用 class。
而当使用特性类(traits class)时, 也必须使用typename 。参考:实例代码 、细说 C++ Traits Classes
我们知道,在 STL 中,容器与算法是分开的,彼此独立设计,容器与算法之间通过迭代器联系在一起。那么,算法是如何从迭代器类中萃取出容器元素的类型的?没错,这正是我们要说的 traits classes 的功能。
//通用版本
template<class IterT>
struct my_iterator_traits {
typedef typename IterT::value_type value_type;
};
my_iterator_traits 其实就是个类模板,其中包含一个类型的声明。有上面 typename 的基础,相信大家不难理解 typedef typename IterT::value_type value_type 的含义:将迭代器的value_type 通过 typedef 变为 类模板的value_type。
为了测试 my_iterator_traits
能否正确萃取迭代器元素的类型,我们先编写以下的测试函数。
void fun(int a) {
cout << "fun(int) is called" << endl;
}
void fun(double a) {
cout << "fun(double) is called" << endl;
}
void fun(char a) {
cout << "fun(char) is called" << endl;
}
我们通过函数重载的方式,来测试元素的类型。
测试代码如下:
my_iterator_traits<vector<int>::iterator>::value_type a;
fun(a); // 输出 fun(int) is called
my_iterator_traits<vector<double>::iterator>::value_type b;
fun(b); // 输出 fun(double) is called
my_iterator_traits<char*>::value_type c;
fun(c); // 输出 fun(char) is called
为了便于理解,我们这里贴出 vector 迭代器声明代码的简化版本:
//vector代码
template <class T, ...>
class vector {
public:
class iterator {
public:
typedef T value_type;
...
};
...
};
我们来解释 my_iterator_traits<vector<int>::iterator> :: value_type a; 语句的含义。
我们先看简单的后半部分:vector<int>::iterator 为vector<int> 的迭代器,该迭代器包含了 value_type 的声明,由 vector 的代码可以知道 T 为 int 类型,所以该迭代器的value_type 即为 int 类型。
接着,回到前半部分:my_iterator_traits<vector<int>::iterator> 会采用 my_iterator_traits 的通用版本,即 typedef typename IterT::value_type value_type 这一类型声明,这里 IterT 为 vector<int>::iterator,故value_type的类型为 vector<int>::iterator::value_type,整个语句萃取出来的类型为 int 类型。
参考书籍:《effective STL》
好了,最后介绍下不能使用typename的例外:就是在继承列表(基类列表)或者成员初始化列表中的基类初始化时,可以不用typename去标识“嵌套依赖类型”。说起来比较绕,看例子就明白了:
template<typename T>
class Derived : public Base<T>::Nested //in base class list, no typename
{
public:
explicit Derived(int x) : Base<T>::Nested(x) //in member init list, no typename
{
typename Base<T>::Nested temp; //nested dependent type, need typename
}
};
其他参考:我必须在何处以及为何要使用“模板”和“typename”关键字?
关于 template、typename、this 关键字的使用:
- 依赖于模板参数(template parameter,形式参数,实参英文为 argument)的名字被称为依赖名字(dependent name),C++标准规定,如果解析器在一个模板中遇到一个嵌套依赖名字,它假定那个名字不是一个类型,除非显式用 typename 关键字前置修饰该名字;
- 和上一条 typename 用法类似,template 用于指明嵌套类型或函数为模板;
- this 用于指定查找基类中的成员(当基类是依赖模板参数的类模板实例时,由于实例化总是推迟,这时不依赖模板参数的名字不在基类中查找)。
一个例子如下(需要 GCC 编译,GCC 对 C++11 几乎全面支持,VS2013 此处总是在基类中查找名字,且函数模板前不需要 template):
#include <iostream>
template<typename T>
class aTMP {
public:
typedef const T reType;
};
void f() {
std::cout << "global f()\n";
}
template<typename T>
class Base {
public:
template <int N = 99>
void f() {
std::cout << "member f(): " << N << '\n';
}
};
template<typename T>
class Derived : public Base<T> {
public:
typename T::reType m; // typename 不能省略
Derived(typename T::reType a) : m(a) { }
void df1() { f(); } // 调用全局 f(),而非想象中的基类 f()
//void df2() { this->template f(); } // 基类 f<99>(),VS无法通过,g++顺利通过
void df2() { Base<T>::template f(); } // 基类 f<99>()
void df3() { Base<T>::template f<22>(); } // 强制基类 f<22>()
void df4() { ::f(); } // 强制全局 f()
};
int main() {
Derived<aTMP<int>> a(10);
a.df1();
a.df2();
a.df3();
a.df4();
return 0;
}
输出结果:
global f()
member f(): 99
member f(): 22
global f()
C++11 关于模板的新特性:
- “>>” 根据上下文自动识别正确语义;
- 函数模板参数默认值;
- 变长模板参数(扩展 sizeof...() 获取参数个数);
- 模板别名(扩展 using 关键字);
- 外部模板实例(拓展 extern 关键字),弃用 export template。
参考资料:
https://accu.org/index.php/journals/442
Effective C++,第三版,Scott Meyers 著,侯捷译
STL 源码剖析,侯捷著
http://www.bogotobogo.com/cplusplus/template_specialization_traits.php
http://www.bogotobogo.com/cplusplus/template_specialization_function_class.php
其他参考文章已在文中蓝色链接编著。