Template Basics
模板的编译检查
编译器对模板的编译检查是分两步的:
在模板定义阶段,模板的检查并不包含类型参数的检查。包含下面几个方面:
- 语法检查。比如少了分号。
- 使用了未定义的不依赖于模板参数的名称(类型名,函数名,......)。
- 未使用模板参数的staticassertions。
在模板实例化阶段,为确保所有代码都是有效的,模板会再次被检查,主要是那些依赖于类型参数的部分,只要会发生模板参数替换的地方,都会进行检查。
template<typename T>
void foo(T t)
{
// 如果 undeclared() 未定义,第一阶段就会报错,因为与模板参数无关
undeclared();
//如果 undeclared(t) 未定义,第二阶段会报错,因为与模板参数有关
undeclared(t);
// 与模板参数无关,总是报错
static_assert(sizeof(int) > 10,"int too small");
//与模板参数有关,只会在第二阶段报错
static_assert(sizeof(T) > 10, "T too small");
}
类型推断中的类型转换
在模板匹配时会进行类型的推断,如:
这里模板参数 Tp
被推导为 int
。在这样的类型推断中,自动的类型转换是受限的:
-
如果形参是引用传值,那么任何类型转换都是不被允许的。
-
如果形参是按值传递,那么会进行退化(decay)这一种简单的转换。
这里退化(decay)指:去 cv 限定符,去引用,原始数组(raw array)和函数转换为响应的指针类型。
template<typename T>
T max (T a, T b);
int const c = 42;
Int i = 1;
max(i, c); // T 被推断为 int,c 中的 const 被 decay 掉
max(c, c); // T 被推断为 int
int& ir = i;
max(i, ir); // T 被推断为 int, ir 中的引用被 decay 掉
int arr[4];
foo(&i, arr); // T 被推断为 int*
模板特化
特化分为全特化和偏特化,全特化是将所有的模板参数都指明,偏特化是只指明部分模板参数。需要注意的是函数模板不能进行偏特化,只能进行重载,只有类模板可以进行偏特化。
函数模板
// 函数模板
template<class Tp1,class Tp2>
void f(Tp1,Tp2) { }
// 全特化
template<>
void f(int,int) {}
// 函数重载, 这会成为一个独立的函数模板
template<class Tp1,class Tp2>
void f(Tp1*,Tp2*) { }
// 函数重载, 这不是偏特化,而是一个独立的函数模板
template<class Tp2>
void f(int,Tp2) { }
在上面调用上面的函数时,重载决议会优先选择普通函数,如果不存在符合的普通函数,就会进行模板函数的重载选择,选择重载的函数模板时,不会考虑各个模板的全特化版本,重载决议发生在主模板之间,决议出符合的主模板后,才会具体选择模板的全特化版本。
我们看下面这个例子:
// 函数模板
template<class Tp1,class Tp2>
void f(Tp1,Tp2) { std::cout << "f(Tp1,Tp2)" << std::endl;}
// 全特化 ,选择 f(Tp1,Tp2) 为主模板
template<>
void f(int,int) { std::cout << "f(int,int)" << std::endl;}
// 函数重载, 这不是偏特化,而是一个独立的函数模板
template<class Tp2>
void f(int,Tp2) { std::cout << "f(int,Tp2)" << std::endl; }
int main()
{
f('a','c'); // 调用 f(Tp1,Tp2)
f(1, 2); // 调用 f(int,Tp2)
f(1, 'c'); // 调用 f(int,Tp2)
return 0;
}
/*
输出结果:
f(Tp1,Tp2)
f(int,Tp2)
f(int,Tp2)
*/
我们发现 f(1,2)
调用的是 f(int,Tp2)
,这就是因为在选择主模板时就已经选择了 f(int,Tp2)
,而非 f(Tp1,Tp2)
,所以它的全特化版本 f(int,int)
就不会被调用。
这里要注意的是全特化版本的函数,在选择主模板时,会向上查找最符合的主模板函数,所以如果我们改一下 f(int,int)
的位置就可以调用到它了:
// 函数模板
template<class Tp1,class Tp2>
void f(Tp1,Tp2) { std::cout << "f(Tp1,Tp2)" << std::endl;}
// 函数重载, 这不是偏特化,而是一个独立的函数模板
template<class Tp2>
void f(int,Tp2) { std::cout << "f(int,Tp2)" << std::endl; }
// 全特化 ,选择 f(int,Tp2) 为主模板
template<>
void f(int,int) { std::cout << "f(int,int)" << std::endl;}
int main()
{
f('a','c'); // 调用 f(Tp1,Tp2)
f(1, 2); // 调用 f(int,int)
f(1, 'c'); // 调用 f(int,Tp2)
return 0;
}
/*
输出结果:
f(Tp1,Tp2)
f(int,int)
f(int,Tp2)
*/
函数模板可以在任意位置给默认参数,这与函数参数列表的默认值不同,也与类模板的默认参数不同,下文会介绍类模板的默认参数。
类模板
类是支持全特化和偏特化的。
// 主模板类
template<class Tp,int sz>
class A{ };
// 偏特化
template<int sz>
class A<char,sz> { };
// 偏特化
template<class Tp,int sz>
class A<Tp*,sz> { };
// 全特化
template<>
class A<double,20> { };
类模板的模板参数,一旦有一个是默认参数,则其后的参数都需要有默认值。并且要注意的的是偏特化的模板类中是不能提供默认模板参数的:
并且偏特化的类会继承主模板的默认模板参数:
// 主模板类
template<class Tp,int sz = 10>
class A{ };
// 偏特化
template<int sz>
class A<char,sz> // sz 的默认值依然是 10
{
public:
A()
{ std::cout << sz << std::endl; }
};
int main()
{
A<char> a;
return 0;
}
/*
输出结果:
10
*/
非类型模板参数
从上文的例子中也可以看到,模板参数不一定是类型,也可以是类似函数参数的参数,当然也可以有默认值:
但并不是所有的类型都可以作为模板参数,根据 C++ 标准,非类型模板参数可以是以下几种类型:
-
整型常量(例如 int, char, bool 等)
-
枚举类型
-
指向对象或函数的指针
-
引用类型
-
指向成员的指针
-
std::nullptr_t 类型的值
从 C++17 开始,支持将非类型模板参数设置为 auto
这样的自动类型:
要注意的是指定非类型模板参数的值时,只能指定编译期就能求值的表达式,否则会报错,这时因为模板的参数要在编译时就确定,才能让编译器去生成对应的函数实例。
模板的分文件编写
对于一般的函数我可能会将其进行定义和声明的分离编写,即声明写道 .h 文件中,定义写到 .cpp 文件中,但是对模板来说直接这样写会有一些问题。
按照一般的分离编译写法如上。
在编译时,在调用模板的地方会在 main.cpp 文件中找对应的函数地址,但是只能找到声明,没有定义,没办法进行函数实例化,就得不到对应的地址,所以只能等到链接的时候到其他文件或库中找该函数的地址。
但是到链接的过程时,却根本找不到对应的函数实现,应为对应模板根本没有实例化,所以就会报链接错误。
解决方法有两种:
- 第一种就是在 .cpp 文件中进行显示实例化
但是这种方法缺陷也很明显,会让代码编写变得非常麻烦,每次需要新的类型都要加上新的显示实例化。
- 第二种就是将模板的定义和声明写道同一个文件中(.h 或 .hpp 文件中),就从根源上解决了该问题,这通常更简单实用。
typename
和 class
通常情况下 typename
和 class
用作 template<>
中声明模板参数的。但是在访问一些还未实例化的类中定义的类型时,也要用到 typename
和 class
。
下面的例子中 list
是自己实现的链表,在其中定义了一个 const_iterator
的迭代器类型:
template<class T>
void print(const myLib::list<T>& a)
{
typename myLib::list<T>::const_iterator it = a.begin();
while(it != a.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
}
当模板还未进行实例化之前,编译器是不清楚 myLib::list<T>::const_iterator
这是一个类型还是 myLib::list<T>
下的一个静态成员变量,所以会发生报错,只有加上 typename
或 class
告诉编译器后面这些是一个变量才能正确编译通过。
模板基类
如果一个类继承自一个模板基类,那么对它即使 x 是继承而来的,使用 this->x
和直接使用 x
也不一定是等效的。比如:
template<class>
class Base
{
public:
void bar(){ }
};
template<class Tp>
class Derived : public Base<Tp>
{
public:
void foo() {
bar(); // 调用外部函数
}
};
这里 bar()
不会被解析为 Base
中的 bar()
,这样要么报错找不到 bar()
,要么调用一个全局的 bar()
导致一些意料之外的错误。要调用 Base
中的 bar()
就要用 this->x
或者 Base<Tp>::
来调用它。