模板template用法
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
模板template⽤法
模板(Template)指C++程序设计设计语⾔中采⽤类型作为参数的程序设计,⽀持通⽤程序设计。
C++ 的标准库提供许多有⽤的函数⼤多结合了模板的观念,如STL以及IO Stream。
参考:
函数模板定义⼀族函数。
//template1.cpp #include <iostream>
template<typename T> void swap(T &a, T &b) {
T tmp{a}; a = b;
b = tmp;
}
int main(int argc, char* argv[])
{
int a = 2; int b = 3;
swap(a, b); // 使⽤函数模板
std::cout << "a=" << a << ", b=" << b << std::endl;
double c = 1.1; double d = 2.2; swap(c, d);std::cout << "c=" << c << ", d=" << d << std::endl;return 0;
}
函数模板的格式:
template<parameter-list> function-declaration
函数模板在形式上分为两部分:模板、函数。
在函数前⾯加上 template<...>就成为函数模板,因此对函数的各种修饰(inline、constexpr 等)需要加在 function-declaration 上,⽽不是 template 前。
如
template
inline T min(const T &, const T &);
parameter-list 是由英⽂逗号(,)分隔的列表,每项可以是下列之⼀:
上⾯ swap 函数模板,使⽤了类型形参。
函数模板就像是⼀种契约,任何满⾜该契约的类型都可以做为模板实参。
⽽契约就是函数实现中,模板实参需要⽀持的各种操作。
上⾯
swap 中 T 需要满⾜的契约为:⽀持拷贝构造和赋值。
C++ 的函数模板本质上函数的重载,泛型只是简化了程序员的⼯作,让这些重载通过编译器来完成。
我们可以⽤ c++flit 来观察这⼀现象。
通过 objdump 可以把 template1.out 中代码段的与 swap 相关的符号提出来,有两个,分别是_Z4swapIdEvRT_S1_和
_Z4swapIiEvRT_S1_
再⽤ f++filt 将 Name Mangling 后的名字翻转过来,就对应了两个函数原型:
void swap(double&, double&)
void swap(int&, int&)
1.2 函数模板不是函数
刚才我们提到函数模板⽤来定义⼀族函数,⽽不是⼀个函数。
C++是⼀种强类型的语
⾔,在不知道 T 的具体类型前,⽆法确定 swap 需要占⽤的栈⼤⼩(参数栈,局部变量),同时也不知道函数体中 T 的各种操作如何实现,⽆法⽣成具体的函数。
只有当⽤具体
类型去替换 T 时,才会⽣成具体函数,该过程叫做函数模板的实例化。
当在 main 函数中调⽤ swap(a,b)时,编译器推断出此时 T 为 int,然后编译器会⽣成 int 版的 swap 函数供调⽤。
所以相较普通函数,函数模板多了⽣成具体函数这⼀步。
如果我们只是编
写了函数模板,但不在任何地⽅使⽤它(也不显式实例化),则编译器不会为该函数模板⽣成任何代码。
函数模板实例化分为隐式实例化和显式实例化。
1.3 隐式实例化 implicit instantiation
仍以 swap 为例,我们在main 中调⽤ swap(a,b)时,就发⽣了隐式实例化。
当函数模板被调⽤,且在之前没有显式实例化时,即发⽣函数模板的隐式实例化。
如果模板实参能从调⽤的语境中推导,则不需要提供。
效率较低。
//template2.cpp #include
template void print(const T &r) {
std::cout << r << std::endl;
}
int main() {
// 隐式实例化print(int) print(1);
// 实例化 print(char) print<>('c');
// 仍然是隐式实例化,我们希望编译器⽣成print(double) print(1);
return 0;
}
1.4 显式实例化 explicit instantiation
隐式实例化可能影响效率,所以需要提⾼效率的显式实例化,显式实例化在编译期间就会⽣成实例(增加了编译时间)。
在函数模板定义后,我们可以通过显式实例化的⽅式告诉编译器⽣成指定实参的函数。
显式实例化声明会阻⽌隐式实例化。
如果我们在显式实例化时,只指定部分模板实参,则指定顺序必须⾃左⾄右依次指定,不能越过前参模板形参,直接指定后⾯的。
1.5. 模板函数的隐式/显⽰化与特化辨析
如果你真的想要实例化(⽽不是特殊化或某物)的功能,请执⾏以下操作:
template void func(T param) {} // definition
template void func(int param); // explicit instantiation.
template <> void func(int param) {} // specialization
总结⼀下,C++只有模板显式实例化(explicit instantiation),隐式实例化(implicit instantiation),特化(specialization,也译作具体化,偏特化)。
⾸先考虑如下模板函数代码:
template
void swap(T &a, T &b){
...
}
1.隐式实例化
我们知道,模板函数不是真正的函数定义,他只是如其名提供⼀个模板,模板只有在运⾏时才会⽣成相应的实例,隐式实例化就是这种情况: int main(){
....
swap(a,b);
....
}
它会在运⾏到这⾥的时候才⽣成相应的实例,很显然的影响效率这⾥顺便提⼀下 swap<int>(a,b);中的<int>是可选的,因为编译器可以根据函数参数类型⾃动进⾏判断,也就是说如果编译器不不能⾃动判断的时候这个就是必要的;
2.显式实例化
前⾯已经提到隐式实例化可能影响效率,所以需要提⾼效率的显式实例化,显式实例化在编译期间就会⽣成实例,⽅法如下:
template void swap(int &a,int &b);
这样就不会影响运⾏时的效率,但编译时间随之增加。
3.特化
这个 swap 可以处理⼀些基本类型如 long int double,但是如果想处理⽤户⾃定义的类型就不⾏了,特化就是为了解决这个问题⽽出现的:
template <> void swap(job a,job b){...}
其中 job 是⽤户定义的类型.
2. 函数模板的使⽤
2.1 使⽤⾮类型形参
//template3.cpp
#include
// N 必须是编译时的常量表达式
template
void printArray(const T (&a)[N]) {
std::cout << "[";
const char *sep = "";
for (int i = 0; i < N; i++, (sep = ", ")) {
std::cout << sep << a[i];
}
std::cout << "]" << std::endl;
}
int main() {
// T: int, N: 3
int a[]={1, 2, 3};
printArray(a);
float b[] = {1.1, 2.2, 3.3};
printArray(b);
return 0;
}
2.2返回值为 auto
有些时候我们会碰到这样⼀种情况,函数的返回值类型取决于函数参数某种运算后的类型。
对于这种情况可以采⽤ auto 关键字作为返回值占位符。
decltype 操作符⽤于查询表达式的数据类型,也是 C++11 标准引⼊的新的运算符,其⽬的是解决泛型编程中有些类型由模板参数
决定,⽽难以表⽰的问题。
为何要将返回值后置呢?
// 这样是编译不过去的,因为 decltype(a*b)中,a 和 b 还未声明,编译器不知道 a 和 b 是什么。
template
decltype(a*b) multi(T a, T b) {
return a*+ b;
}
//编译时会产⽣如下错误:error: use of undeclared identifier 'a'
//template4.cpp
#include
using namespace std;
template
auto multi(T1 a, T2 b) -> decltype(a * b) {
return a * b;
}
int main(int argc, char* argv[])
{
cout << multi(2, 3) << endl;
cout << multi(2.2, 3.0) << endl;
cout << multi(2.2, 4) << endl;
cout << multi(3, 4.4) << endl;
return 0;
}
2.3 类成员函数模板
函数模板可以做为类的成员函数。
需要注意的是:函数模板不能⽤作虚函数。
这是因为 C++编译器在解析类的时候就要确定虚函数表(vtable)的⼤⼩,如果允许⼀个虚函数是函数模板,那么就需要在解析这个类之前扫描所有的代码,找出这个模板成员函数的调⽤或显式实例化操作,然后才能确定虚函数表的⼤⼩,⽽显然这是不可⾏的。
//template5.cpp
#include <iostream>
class object {
public:
template<typename T>
void print(const char *name, const T &v){
std::cout << name << ": " << v << std::endl;
}
};
int main() {
object o;
o.print("name", "Crystal");
o.print("age", 18);
return 0;
}
2.4 函数模板重载
函数模板之间、普通函数和模板函数之间可以重载。
编译器会根据调⽤时提供的函数参数,调⽤能够处理这⼀类型的最佳匹配版本。
在匹配度上,⼀般按照如下顺序考虑:
可以通过空模板实参列表来限定编译器只匹配函数模板,⽐如 main 函数中的最后⼀条语句。
//template6.cpp
#include
#include
template
const T &max(const T &a, const T &b)
{ std::cout << "max(&, &) = ";
return a > b ? a : b;
}
// 函数模板重载
template
const T *max(T *a, T *b) {
std::cout << "max(*, *) = ";
return *a > *b ? a : b;
}
// 函数模板重载
template
const T &max(const T &a, const T &b, const T &c)
{ std::cout << "max(&, &, &) = ";
const T &t = (a > b ? a : b);
return t > c ? t : c;
}
// 普通函数
const char *max(const char *a, const char *b)
{ std::cout << "max(const char *, const char *) = ";
return strcmp(a, b) > 0 ? a : b;
}
int main() {
int a = 1, b = 2;
std::cout << max(a, b) << std::endl;
std::cout << *max(&a, &b) << std::endl;
std::cout << max(a, b, 3) << std::endl;
std::cout << max("en", "ch") << std::endl;
// 可以通过空模板实参列表来限定编译器只匹配函数模板
std::cout << max<>("en", "ch") << std::endl;
std::cout << max(100, 200) << std::endl;
return 0;
}
2.5 函数模板特化 specialization
当函数模板需要对某些类型进⾏特别处理,这称为函数模板的特化。
当我们定义⼀个特化版本时,函数参数类型必须与⼀个先前声明的模板中对应的类型匹配。
函数模板特化的本质是实例化⼀个模板,⽽⾮重载它。
因此,特化不影响编译器函数匹配。
//template7.cpp
#include
#include
using namespace std;
template
int compare(const T1 &a, const T2 b) {
return a - b;
}
// 对 const char *进⾏特化
template<>
int compare(const char * const &a, const char * const &b) {
return strcmp(a, b);
}
int main(int argc, char* argv[])
{
cout << compare(100, 200) << endl;
cout << compare("abc", "xyz") << endl;
return 0;
}
上⾯的例⼦中针对 const char *的特化,我们其实可以通过函数重载达到相同效果。
因此对于函数模板特化,⽬前公认的观点是没什么⽤,并且最好别⽤。
Why Not Specialize Function Templates?
但函数模板特化和重载在重载决议时有些细微的差别。
这些差别中⽐较有⽤的⼀个是阻⽌某些隐式转换。
如当你只有 void foo(int)时,以浮点类型调⽤会发⽣隐式转换,这可以通过特化来阻⽌:
template <class T>void foo(T);
template <> void foo(int) {}
foo(3.0); // link error,阻⽌ float 隐式转换为 int
虽然模板配重载也可以达到同样的效果,但特化版的意图更加明确。
函数模板及其特化版本应该声明在同⼀个头⽂件中。
所有同名模板的声明应该放在前⾯,然后是这些模板的特化版本。
程序运⾏结果和使⽤函数模板特化相同。
但是,使⽤普通函数重载和使⽤模板特化还是有不同之处,主要表现在如下两个⽅⾯:
(1)如果使⽤普通重载函数,那么不管是否发⽣实际的函数调⽤,都会在⽬标⽂件中⽣成该函数的⼆进制代码。
⽽如果使⽤模板的特化版本,除⾮发⽣函数调⽤,否则不会在⽬标⽂件中包含特化模板函数的⼆进制代码。
这符合函数模板的“惰性实例化”准则。
(2)如果使⽤普通重载函数,那么在分离编译模式下,应该在各个源⽂件中包含重载函数的申明,否则在某些源⽂件中就会使⽤模板函数,⽽不是重载函数。
2.6 变参函数模板(模板参数包)
这是 C++11 引⼊的新特性,⽤来表⽰任意数量的模板形参。
其语法样式如下:
template<typename ...Args> // Args: 模板参数包
void foo(Args ... args);// args: 函数参数包
在模板形参 Args 的左边出现三个英⽂点号"...",表⽰ Args 是零个或多个类型的列表,是⼀个模板参数包(template parameter pack)。
正如其名称⼀样,编译器会将 Args 所表⽰的类型列表打成⼀个包,将其当做⼀个特殊类型处理。
相应的函数参数列表中也有⼀个函数参数包。
与普通模板函数⼀样,编译器从函数的实参推断模板参数类型,与此同时还会推断包中参数的数量。
// sizeof...() 是 C++11 引⼊的参数包的操作函数,⽤来取参数的数量
template
int length(Args ... args) {
return sizeof...(Args);
}
// 以下语句将在屏幕打印出:2
std::cout << length(1, "hello") << std::endl;
变参函数模板主要⽤来处理既不知道要处理的实参的数⽬也不知道它们的类型时的场景。
既然我们对实参数量以及类型都⼀⽆所知,那么我们怎么使⽤它呢?最常⽤的⽅法是递归。
//template8.cpp
#include
using namespace std;
// sizeof...() 是 C++11 引⼊的参数包的操作函数,⽤来取参数的数量
template
int length(Args ... args) {
return sizeof...(Args);
}
int main(int argc, char* argv[])
{
// 以下语句将在屏幕打印出:2
cout << length(1, "hello") << endl; // 以下语句将在屏幕打印出:3
cout << length(1, "hello", 2) << endl; // 以下语句将在屏幕打印出:4
cout << length(1, "hello", 2, 3) << endl; // 以下语句将在屏幕打印出:5
cout << length(1, "hello", 2, 3, 4) << endl;
return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <vector>
#include <map>
// 在C++11之前,类模板和函数模板只能含有固定数量的模板参数。
C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。
// 可变参数模板类继承⽅式展开参数包
// 可变参数模板类的展开⼀般需要定义2 ~ 3个类,包含类声明和特化的模板类
template<typename... A> class BMW{}; // 变长模板的声明
template<typename Head, typename... Tail> // 递归的偏特化定义
class BMW<Head, Tail...> : public BMW<Tail...>
{//当实例化对象时,则会引起基类的递归构造
public:
BMW()
{
printf("type: %s\n", typeid(Head).name());
}
Head head;
};
template<> class BMW<>{}; // 边界条件
// 模板递归和特化⽅式展开参数包
template <long... nums> struct Multiply;// 变长模板的声明
template <long first, long... last>
struct Multiply<first, last...> // 变长模板类
{
static const long val = first * Multiply<last...>::val;
};
template<>
struct Multiply<> // 边界条件
{
static const long val = 1;
};
void mytest()
{
BMW<int, char, float> car;
/*
运⾏结果:
type: f
type: c
type: i
*/
std::cout << Multiply<2, 3, 4, 5>::val << std::endl; // 120
return;
}
int main()
{
mytest();
system("pause");
return 0;
}
类模板
考虑我们写⼀个简单的栈的类,这个栈可以⽀持int类型,long类型,string类型等等,不利⽤类模板,我们就要写三个以上的stack类,其中代码基本⼀样,通过类模板,我们可以定义⼀个简单的栈模板,再根据需要实例化为int栈,long栈,string栈。
statck.h
template <class T> class Stack {
public:
Stack();
~Stack();
void push(T t);
T pop();
bool isEmpty();
private:
T *m_pT;
int m_maxSize;
int m_size;
};
#include "stack.cpp"
//stack.cpp
template <class T> Stack<T>::Stack(){
m_maxSize = 100;
m_size = 0;
m_pT = new T[m_maxSize];
}
template <class T> Stack<T>::~Stack() {
delete [] m_pT ;
}
template <class T> void Stack<T>::push(T t) {
m_size++;
m_pT[m_size - 1] = t;
}
template <class T> T Stack<T>::pop() {
T t = m_pT[m_size - 1];
m_size--;
return t;
}
template <class T> bool Stack<T>::isEmpty() {
return m_size == 0;
}
模板参数
模板可以有类型参数,也可以有常规的类型参数int,也可以有默认模板参数,例如
template<class T, T def_val> class Stack{...}
上述类模板的栈有⼀个限制,就是最多只能⽀持100个元素,我们可以使⽤模板参数配置这个栈的最⼤元素数,如果不配置,就设置默认最⼤值为100。