C++模板及实例化与具体化
实例化(instantiation)
通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生出一个不同的实体。因此,针对 3 种类型中的每一种,getMax()都被编译了一次。
这种用具体类型代替模板参数的过程叫做实例化。它产生了一个模板的实例。
可以理解为编译器使用模板为特定类型生成函数(类)定义时,得到的就是模板实例。
只要使用函数(类)模板,(编译器)会自动地引发这样一个实例化过程,因此程序员并不需要额外地请求模板的实例化。
getMax.hpp
template <typename T>
inline T const& getMax (T const& a, T const& b)
{
return a < b ? b : a;
}
getMax.cpp
#include <iostream>
#include <string>
#include "getMax.hpp"
int main()
{
int i = 42;
std::cout << "getMax(7,i): " << getMax(7,i) << std::endl;//使用了以int作为模板参数T的函数模板
std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "getMax(s1,s2): " << getMax(s1,s2) << std::endl;
}
具体化(specialization)
模板的具体化(specialization)分为隐式实例化(implicit instantiation)、显式实例化(explicit instantiation)和显式具体化(explicit specialization)。模板以泛型的方式描述函数(类),而具体化是使用具体的类型生成函数(类)声明。
显式实例化和显式具体化的区别在于显式实例化只需要写声明不需要写定义,显式实例化的定义与隐式实例化一样由编译器生成。
显式具体化也可以称为显式特化或全局特化,除了声明还必须写定义,也就是说可以选择修改基本模板的功能。可以说特化和基本模板只有名称有关联。
隐式实例化(implicit instantiation)
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的函数(类)定义,这是我们最常用的使用方式。具体代码查看上面的getMax。
显式实例化(explicit instantiation)
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中。
例如,声明一个类:
template <typename T>
class Stack {
private:
std::vector<T> elems;
};
template class Stack<std::string>; //显式实例化,指定编译器生成std::string类型的模板定义
或声明一个函数:
template <typename T>
inline T max (T a, T b)
{
return a < b ? b : a;
}
template int max<int>(int a, int b); //显式实例化,指定编译器生成int类型的模板定义
template int max<>(int a, int b); //简写
template int max(int a, int b); //简写
上述的代码是一种手工实例化模板的机制:显式实例化指示符(explicit instantiation directive)。显式实例化指示符由关键字template和紧接其后的我们所需要实例化的实体(可以是类、函数、成员函数等)的声明组成,而且,该声明是一个已经用实参完全替换参数之后的声明。该指示符适用于普通函数、成员函数和静态数据成员。
在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
总结:显式实例化只是一个声明,让编译器用具体的类型生成函数定义或类定义。
PS:显示实例化不允许在函数内声明,否则报错:编译器错误 C2252:不能在当前范围内显式实例化模板
编译器错误 C2252
显式实例化的作用
既然显示实例化只是一个声明,最终生成定义的还是编译器,那显示实例化的作用何在呢?让我们来看一下下面的代码
MyTemplate.h
template <typename T>
void fun(const T& t);
MyTemplate.cpp
#include "MyTemplate.h"
#include <iostream>
template <typename T>
void fun(const T& t)
{
std::cout << "fun" << std::endl;
}
template void fun<int>(const int& t);//显式实例化
main.cpp
#include "MyTemplate.h"
int main()
{
fun(1);
return 0;
}
有了解或用过模板的都知道,最常用的模板使用方法是包含模型(inclusion model),也就是在.hpp里同时写上模板的声明和定义,而不能分开在.h和.cpp里分别写声明与定义,如果我们注释掉//显式实例化这一行,会导致编译错误 LNK2019: 无法解析的外部符号。
这样会导致一个问题,包含的头文件会变得庞大,而且include的头文件中可能还有模板文件。
所以使用显式实例化的优点是:避免包含庞大头文件的开销,更可以把模板定义的源文件封装起来。而缺点是:每次使用新的模板参数时,都需要手动进行实例化,否则就会产生编译错误 LNK2019。
显式具体化(explicit specialization)、特化
类模板的特化
你可以用模板实参来特化类模板。和函数模板的重载类似,通过特化类模板,你可以优化基于某种特定类型的实现,或者克服某种特定类型在实例化类模板时所出现的不足。另外,如果要特化一个类模板,你还要特化该类模板的所有成员函数。虽然也可以只特化某个成员函数,但这个做法并没有特化整个类,也就没有特化整个类模板。
全局特化的实现并不需要与(原来的)泛型实现有任何关联,这就允许我们可以包含不同名称的成员函数(info相对msg)。实际上,全局特化只和类模板的名称有关联。
为了特化一个类模板,你必须在起始处声明一个 template<>,接下来声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:
template<>
class Stack<std::string> {
}
...
进行类模板的特化时,每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代:
void Stack<std::string>::push (std::string const& elem)
{
elems.push_back(elem); //附加传入实参elem的拷贝
}
类模板特化例子1
Stack模板,Stack1.hpp
#include <vector>
#include <stdexcept>
template <typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
void push(T const&); // push element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
template <typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
特化Stack,Stack2.hpp
其中elems不使用std::vector而使用了std::deque,说明特化的实现可以和基本类模板(prinmary template)的实现完全不同。
#include <deque>
#include <string>
#include <stdexcept>
#include "stack1.hpp"
template<>
class Stack<std::string> {
private:
std::deque<std::string> elems; // elements
public:
void push(std::string const&); // push element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
void Stack<std::string>::push (std::string const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
类模板特化例子2
指定的模板实参列表必须和相应的模板参数列表一一对应。例如,我们不能用一个非类型值来替换一个模板类型参数。然而,如果模板参数具有缺省模板实参,那么用来替换的模板实参就是可选的(即不是必须的)
template<typename T>
class Types {
public:
typedef int I;
};
template<typename T, typename U = typename Types<T>::I>
class S; // (1)
template<>
class S<void> { // (2)
public:
void f();
};
template<> class S<char, char>; // (3)
template<> class S<char, 0>; // 错误:不能用0来替换U
int main()
{
S<int>* pi; // 正确:使用(1),这里不需要定义
S<int> e1; // 错误:使用(1),需要定义,但找不到定义
S<void>* pv; // 正确:使用(2)
S<void,int> sv; // 正确:使用(2),这里定义是存在的
S<void,char> e2; // 错误:使用(1),需要定义,但找不到定义
S<char,char> e3; // 错误:使用(3),需要定义,但找不到定义
}
template<>
class S<char, char> { // (3)处的定义
};
重复定义的错误
全局模板特化和由模板生成的实例化版本是不能够共存于同一个程序中的。如果试图在同一个文件中使用这两者的话,那么通常都会导致一个编译期错误
#include <iostream>
template <typename T>
class Invalid {
};
template <typename T>
class Invalid2 {
public:
Invalid2(const T& t)
{
std::cout << "typename T" << std::endl;
}
};
// 错误:Invalid<double>已经被实例化了
template<>
class Invalid<double>
{
};
template <>
class Invalid2<double> {
public:
Invalid2(const double& t)
{
std::cout << "typename double" << std::endl;
}
};
int main()
{
Invalid<double> x1; // 产生一个Invalid<double>实例化体
Invalid2 x2(1.1); //typename double
}
函数模板的特化
就语法及其后所蕴涵的原则而言,(显式的)全局函数模板特化和类模板特化大体上是一致的,唯一的区别在于:函数模板特化引入了重载和实参演绎这两个概念。
函数模板特化简写
template<typename T>
int f(T) { return 1; }
template<> int f<int>(int) { return 3; } //完整写法
template<> int f(int) { return 3; } //简写,省略了<int>
函数模板特化例子1
如果可以借助实参演绎(用实参类型来演绎声明中给出的参数类型)来确定模板的特殊化版本,那么全局特化就可以不声明显式的模板实参。让我们考虑下面的例子:
template<typename T>
int f(T) // (1)
{
return 1;
}
template<typename T>
int f(T*) // (2)
{
return 2;
}
template<> int f(int) // OK: (1)的特化
{
return 3;
}
template<> int f(int*) // OK: (2)的特化。
{
return 4;
}
函数模板特化例子2
全局函数模板特化不能包含缺省的实参值。然而,对于基本(即要被特化的)模板所指定的任何缺省实参,显式特化版本都可以应用这些缺省实参值。例如:
template<typename T>
int f(T, T x = 42)
{
return x;
}
template<> int f(int, int = 35) // 错误,不能包含缺省实参值
{
return 0;
}
template<typename T>
int g(T, T x = 42)
{
return x;
}
template<> int g(int, int y)
{
return y/2;
}
int main()
{
std::cout << g(0) << std::endl; // 正确,输出21,y默认为42
}
部分具体化(partial specialization)、部分特化
类模板的局部特化
类模板可以被局部特化。你可以在特定的环境下指定类模板的特定实现,并且要求某些模板参数仍然必须由用户来定义。
例如类模板:
template <typename T1, typename T2>
class MyClass {
...
};
就可以有下面几种局部特化:
//局部特化:两个模板参数具有相同的类型
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*>{
...
};
下面的例子展示各种声明会使用哪个模板:
Myclass<int,float> mif; //使用MyClass<T1,T2>
MyClass<float,float> mff; //使用MyClass<T,T>
MyClass<float,int> mfi; //使用MyClass<T,int>
MyClass<int*,float*> mp; //使用MyClass<T1*,T2*>
如果有多个局部特化同等程度地匹配某个声明,那么就称该声明具有二义性:
MyClass<int,int> m; //错误:同等程度地匹配MyClass<T,T>和MyClass<T,int>
函数模板的局部特化
template <typename T>
void Swap (T &,T &) { std::cout << "template generics" << std::endl; }
//局部特化
template <typename T>
void Swap (T* &,T* &) { std::cout << "partial specialization" << std::endl; }
综合例子
类模板
#include <iostream>
#include <string>
template <typename T1, typename T2>
class MyClass
{
public:
MyClass(const T1& t1, const T2& t2) { std::cout << "template MyClass : " << t1 << std::endl; }
};
//显示具体化、特化
template<>
class MyClass<std::string, std::string>
{
public:
MyClass(const std::string& t1, const std::string& t2) { std::cout << "explicit specialization : " << t1 << std::endl; }
};
//部分特化
template <typename T1>
class MyClass<T1, double>
{
public:
MyClass(const T1& t1, const double& t2) { std::cout << "partial specialization : " << t1 << std::endl; }
};
//显示实例化
template class MyClass<std::string, int>;
int main()
{
MyClass<int, double> * class1; //编译器在需要对象之前,不会生成类的隐式实例化
MyClass class2("s2", "s2-2"); //隐式实例化,这里调用的是 MyClass<char [3], char [5]>,而非特化的 MyClass<std::string, std::string>
std::string s3("s3"), s4("s4"), s5("s5");
MyClass class3(s3, 3); //调用显示实例化
//MyClass<std::string, std::string> class4(s4, s4); //显示具体化
MyClass class4(s4, s4); //与上行相同
MyClass class5(s5, 3.14); //部分特化
return 0;
}
输出
template MyClass : s2
template MyClass : s3
explicit specialization : s4
partial specialization : s5
函数模板
#include <iostream>
#include <string>
template <typename T>
void Swap (T &,T &) { std::cout << "template generics" << std::endl; }
//局部特化
template <typename T>
void Swap (T* &,T* &) { std::cout << "partial specialization" << std::endl; }
//显示具体化、特化
template <> void Swap<std::string> (std::string &, std::string &) { std::cout << "template std::string" << std::endl; }
//显示实例化
template void Swap<double>(double &, double &);
int main (void)
{
int a1, b1;
Swap(a1, b1); //隐式实例化
double a3, b3;
Swap(a3, b3); //显示实例化
std::string a2, b2;
Swap(a2, b2); //显示具体化、特化
int* a4; int* b4;
Swap(a4, b4); //局部特化
return 0;
}
重载函数模板
和普通函数一样,函数模板也可以被重载。就是说,相同的函数名称可以具有不同的函数定义;于是,当使用函数名称进行函数调用的时候,C++编译器必须决定究竟要调用哪个候选函数。
//求两个int值的最大值
inline int const& max (int const& a, int const& b)
{
return a < b ? b : a;
}
// 求两个任意类型值中的最大者
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
// 求3个任意类型值中的最大者
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
return ::max (::max(a,b), c);
}
int main()
{
::max(7, 42, 68); // 调用具有3个参数的模板
//对于非模板函数和同名的函数模板,如果其他条件都是相同的话,那么在调用的时候,重载解析过程通常会调用非模板函数,而不会从该模板产生出一个实例。
::max(7, 42); // 调用int重载的非模板函数
//如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
::max(7.0, 42.0); // 调用max<double> (通过实参演绎)
::max('a', 'b'); // 调用max<char> (通过实参演绎)
//显式地指定一个空的模板实参列表,这个语法好像是告诉编译器:只有模板才能来匹配这个调用,而且所有的模板参数都应该根据调用实参演绎出来
::max<>(7, 42); // 调用 max<int> (通过实参演绎)
::max<double>(7, 42); // 调用max<double> (没有实参演绎)
//因为模板是不允许自动类型转化的;但普通函数可以进行自动类型转换,所以最后一个调用将使用非模板函数(‘a’和42.7都被转化为int)
::max('a', 42.7); // 调用int重载的非模板函数
}
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
参考书籍
C++ Templates
C++ Primer Plus (第6版)