C++另一种编程思想称为泛型编程,主要技术就是模板;
C++提供两种模板机制:函数模板和类模板
// 作用 建立一个通用函数,其函数返回值类型和形参类型可以不用具体制定 用一个虚拟的类型来代表(类型参数化); // 语法 template函数声明或定义; template -- 声明创建模板; typename -- 表明其后面的符号是一种数据类型,可以用class代替 T -- 通用的数据类型,名称可以替换,通常为大写字母
templatevoid swap(T &a, T &b) { T temp = a; a = b; b = temp; } // 使用 int main(){ int a = 10; int b = 20; // 自动类型推导 swap(a, b); // 显示指定类型 swap (a,b); }
// 注意事项 自动类型推导,必须推导出一致的数据类型T才可以使用; 模板必须要确定出T的数据类型,才可以使用;普通函数和函数模板的区别
普通函数调用时可以发生自动类型转换(隐式类型转换)
函数模板调用时:
**模板<数据类型>(…):**不能发生类型转换,必须为指定类型
**模板<>(…):**自动推断合适的模板,也不能发生类型转换
// 如果函数模板和普通函数都可以实现,优先调用普通函数
// 可以通过空模板参数列表来强制调用函数模板
// 函数模板可以发送重载
// 如果函数模板可以产生更好的匹配,优先调用函数模板
void func(int a,int b){
cout << "普通函数" << endl;
}
template
void func(T a,T b){
cout << "函数模板" << endl;
}
int main(){
int a = 10;
int b = 10;
func(a,b); // 优先调用普通函数
func<>(a,b); // 强制调用函数模板
}
// 总结:既然提供了函数模板,就不要在提供普通函数了,不然容易出现二义性
模板的局限性
// 有些特定的自定义数据类型,需要用具体化方法做特殊实现
class A{
public:
A(int x,int y):a(x),b(y){}
int a;
int b;
};
template
bool func(T &a,T &b){
if (a == b){
return true;
}
return false;
}
// 利用具体化A类,来实现重载比较
template<>bool func(A &a,A &b){
if (a.a == b.a && a.b == b.b){
return true;
}
return false;
}
int main(){
A a1(10,10);
A a2(20,10);
bool a = func(a1,a2); // 走具体实现
}
控制实例化
// 显示实例化 // 表示模板会在template的地方实例化,而不是本文件的地方 extern template class Blod重载与模板; // 实例化声明 // 模板会在这里实例化 template int compare(int&,int&); // 实例化定义 // 实例化定义声明不能放在一起 // 可以有多个extern声明,但是只能有一个template定义 // 实例化定义会实例化所有成员
// 匹配中精确匹配优先,需要类型转换的优先级低 // 同样好的匹配,非模板函数优先 // 同样好的匹配,没有非模板函数,特例化的模板优先可变参数模板
// Args是一个模板参数包,表示零或多个类型 template// rest是一个函数参数包,表示零或多个参数 void foo(const T& t, const Args& ... rest) { // 类型参数数目 std::cout << sizeof...(Args) << std::endl; // 函数参数数目 std::cout << sizeof... (rest) << std::endl; }
// 编写可变参数函数模板 // 可变参数函数通常是递归的 templatestd::ostream& print(std::ostream& os, const T& t) { // 终止函数 return os << t; } template std::ostream& print(std::ostream& os, const T& t, const Args&...args) { os << t << ','; // 递归调用 return print(os, args...); }
// 包扩展 // 对于一个参数包,除了能获取大小,唯一能做的就是扩展 // 在模式右边放...号来触发扩展 print(os, show(args)...) // 相当于 print(os, show(a1),show(a2) ... show(an)); // 扩展中的模式会独立地应用于包的每个元素
// 转发包 std::forward模板特例化(args)... // 不管是T&&、左值引用、右值引用, std::forward都会按照原来的类型完美转发。 // forward主要解决引用函数参数为右值时, 传进来之后有了变量名就变成了左值。
// 正常模板 templateint compare(const& T,const& T){...}; // 特例化模板 template<> int compare(const char* const&,const char* const&){...}; // 匹配规则不变 // 注意,一个特例化版本本质上是一个实例,而非函数名重载 // 类模板相同 // 同时,类模板可以只将部分成员特例化,比如成员函数
如果有一个函数模板,它的模板参数既作为参数类型又作为返回类型,那么一定要首先声明函数的返回类型参数,否则就不能省略调用函数参数表中的任何类型的参数。
template类模板R implicit_cast(const P& p) { return p; } int main() { int i = 1; float x = implicit_cast (i); return 0; } // 如果将模板中R和P的位置对换,则上述代码不能通过编译 template R implicit_cast(const P& p) { return p; } int i = 1; // 必须完整调用 float x = implicit_cast (i);
template类模板与函数模板的区别class A{ public: A(NameType name,AgeType age){ this->_name = name; this->_age = age; } NameType _name; AgeType _age; }; int main(){ A a("zxl",18); }
// 类模板没有自动类型推导的使用方式,必须为显示类型指定 // 类模板在模板参数列表中可以有默认参数 // 函数模板和类模板成员函数的定义放在头文件内 // 模板头文件既包含声明也包含定义 template类模板中成员函数的创建时机// 默认AgeType类型为int class A{ public: A(NameType name,AgeType age){ this->_name = name; this->_age = age; } NameType _name; AgeType _age; }; int main(){ A a("zxl",100); // 类模板只能用这种方式实例化 A a("zxl",100); // 加上了默认参数类型 }
普通类中的成员函数一开始就可以创建
类模板中的成员函数在调用时才创建
指定传入的类型 – 直接显示对象的数据类型
参数模板化 – 将对象中的参数变为模板进行传递
整个类模板化 – 将这个对象类型模板化进行传递
template成员模板class A{ public: A(T1 name,T2 age){ this->_name = name; this->_age = age; } T1 _name; T2 _age; }; // 指定传入 void func(A &p); // 参数模板 template void func(A &p); // 类模板化 template void func( T ); int main(){ A a("zxl",100); } // 其中第一种是最常用的
成员模板不能是虚函数
// 类模板的成员模板 template类模板与继承class A { template A(U b,U e); } // 类模板在上,成员模板在下 template template A ::A(U b, U e){};
当子类继承的父类是一个类模板时,子类在声明的时候,要指出父类中T的类型
如果不指定,编译器无法给子类分配内存
如果想灵活指定出父类中T的类型,子类也需要为类模板
template类模板成员函数类外实现class Base{ T m; }; class Son:public Base { // 如果使用这种方法继承,那么父类中的T只能为int类型 }; template class Son1:public Base { // 父类的类型 T1 b; // 子类的类型 } // 总结:如果父类是类模板。子类需要指定出父类中的T的数据类型
需要加上模板
成员模板不能是虚函数
template类模板分文件编写class A{ public: A(T1 name, T2 age); void func(); T1 _name; T2 _age }; // 构造函数的类外实现 template A ::A(T1 name,T2 age){ // 实现体 } // 成员函数的类外实现 template void A ::func(){ // 实现体 }
问题
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决
1.直接包含.cpp源文件;
2.将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定名称,并不是强制的;
主流的解决方法是用第二种
全局函数内类实现 – 直接内类声明
全局函数内外实现 – 特殊方法
一般而言没有特殊需求的化就用类内实现
// 如果是类外实现这两段代码必须放在类的上面 template例子class A; // 成员函数 template void func1(A p) { cout << "类外" << endl; } // 类 template class A { friend void func(A p) { cout << "类内" << endl; } // 类外实现,加一个空模板参数列表 friend void func1<>(A p); public: A(T1 name, T2 age){ this->_name = name; this->_age = age; } private: T1 _name; T2 _age };
#pragma once #include非类型模板参数template class Arr { public: Arr(int cap) { this->_cap = cap; this->_size = 0; this->padd = new T[this->_cap]; } Arr(const Arr& arr) { this->_cap = arr._cap; this->_size = arr._size; this->padd = new T[arr._cap]; for (int i = 0; i < this->_size; i++) { this->padd[i] = arr.padd[i]; } } Arr& operator=(const Arr& arr) { if (this->padd != NULL) { delete[] this->padd; this->padd = NULL; this->_cap = 0; this->_size = 0; } this->_cap = arr._cap; this->_size = arr._size; this->padd = new T[arr._cap]; for (int i = 0; i < this->_size; i++) { this->padd[i] = arr.padd[i]; } return *this; } // 尾插 void push_back(const T& val) { if (this->_cap == this->_size) { return; } this->padd[this->_size] = val; this->_size++; } // 尾删 void pop_back() { if (this->_size == 0) { return; } this->_size--; } // 下标运算 T& operator[](int index) { return this->padd[index]; } ~Arr() { if (this->padd != NULL) { delete[] this->padd; this->padd = NULL; } } private: T* padd; int _cap; int _size; };
// 一个非类型参数表示一个值而非一个类型 // 非类型模板参数的模板实参必须是常量表达式 template模板编译int compare(const char(&p1)[N], const char(&p2)[M]) { return strcmp(p1, p2); }
当编译器遇到一个模板定义时,它并不生产代码
只有当我们实例化出模板的一个特定版本时,编译器才生成代码
templatestruct Factorial { enum { val = Factorial ::val * n }; }; template<> struct Factorial<0> { enum { val = 1 }; }; int main() { // 在编译时就已经计算完成了! // val由整数常量479001600替代 cout << Factorial<12>::val << endl; return 0; }
正如将进行类型参数代替作为一种方便的方法,这意味着产生了一种支持编译时编程的机制。这样的程序成为模板元程序(template metaprogram)。
实际上,模板元编程就是完全的图灵机(Turing complete),因为它支持选择(if-else)和循环(通过递归)。从理论上讲,可以用它执行任何的计算
template表达式模板struct Fib { enum { val = Fib ::val + Fib ::val }; }; template<> struct Fib<1> { enum { val = 1 }; }; template<> struct Fib<0> { enum { val = 0 }; }; int main() { cout << Fib<20>::val << endl; return 0; }
表达式模板能够使某些计算得到全方面的编译时优化,表达式模板允许在没有临时变量的情况下使用同一个表达式。
例如:将三个个矩阵或向量相加
D = A + B + C;
这个表达式将会导致一些临时变量的产生,一个是A+B,一个是(A+B)+ C。当这些变量代表极大的矩阵或者向量时,资源的消耗时难以接受的。
#include#include #include #include using namespace std; // A proxy class for sums of vectors template class MyVectorSum; template class MyVector { private: T data[N]; public: MyVector & operator=(const MyVector & right) { for (size_t i = 0; i < N; ++i) { data[i] = right.data[i]; } return *this; } MyVector & operator=(const MyVectorSum & right); const T& operator[](size_t i) const { return data[i]; } T& operator[](size_t i) { return data[i]; } }; // proxy class hold references. uses lazy addition template class MyVectorSum { private: const MyVector & left; const MyVector & right; public: MyVectorSum(const MyVector & lhs, const MyVector & rhs) : left(lhs), right(rhs) {}; T operator[](size_t i) const { return left[i] + right[i]; } }; // Operator to support v3 = v1 + v2 template MyVector & MyVector ::operator=(const MyVectorSum & right) { for (size_t i = 0; i < N; ++i) { data[i] = right[i]; } return *this; } // operator+ just stores references template inline MyVectorSum operator+(const MyVector & left, const MyVector & right) { return MyVectorSum (left, right); } // test function template void print(MyVector & v) { for (size_t i = 0; i < N; ++i) { cout << v[i] << ' '; } cout << endl; } template void init(MyVector & v) { for (size_t i = 0; i < N; ++i) { v[i] = rand() % 100; } } int main() { srand(time(0)); MyVector v1; init(v1); print(v1); MyVector v2; init(v2); print(v2); MyVector v3; v3 = v1 + v2; print(v3); }
当MyVectorSum类产生时,它并不进行计算;它只是持有两个待加向量的引用,仅当访问一个向量和的成员时,计算才会发生
但这样并不支持多操作,例如:v4 = v1 + v2 + v3
没有定义MyVectorSum + MyVector的操作
可以通过模板解决



