template <typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
max(4, 7); // Success
max(4, 7.2); // Error
因为输入的a是int类型,但是b确实double类型,两个类型不同但是只有一个T,所以报错
max<double>(4, 7.2);
template <typename T1, typename T2>
inline T1 const& max(T1 const& a, T2 const& b)
{
return a < b ? b : a;
}
template<typename T>
inline T max(T a, T b)
template<typename T> 中T就是模板参数max(T a, T b)中的a、b就是调用参数template <typename T1, typename T2, typename T3>
inline T1 const& max(T1 const& a, T3 const& b)
{
return a < b ? b : a;
}
max(1, 2);
自变量推导机制并不对返回类型进行匹配,所以这里不能推导出T1的数据类型,需要手动指出max<int, double, double>(4, 4.2)
template <typename TR, typename T1, typename T2>
inline TR const& max(T1 const& a, T3 const& b)
{
return a < b ? b : a;
}
max<double>(1, 2);
TR是手动指定的double,T1、T2是可以被自变量推导出来的
// 传回两个 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;
}
// 传回三个任意类型值中的最大者
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); // 调用「接受三个自变量」的函数
::max(7.0, 42.0); // 调用 max<double>(经由自变量推导)
::max('a', 'b'); // 调用 max<char>(经由自变量推导)
::max(7, 42); // 调用「接受两个 int 自变量」的 non-template 函数
::max<>(7, 42); // 调用 max<int>(经由自变量推导)
::max<double>(7, 42); // 调用 max<double>(无需自变量推导)
::max('a', 42.7); // 调用「接受两个 int 自变量」的 non-template 函数
return 0;
}
一般来说,不同的重载形式之间最好不存在 绝对必要的差异,各重载形式之间应该只存在 参数个数不同 或 参数类型的明确不同,所以不要出现一个参数是引用、另一个不是引用的情况
声名类模板
template <typename T>
class Stack {
private:
std::vector<T> elems; // 元素
public:
void push(T const&); // push 元素
void pop(); // pop 元素
T top() const; // 传回最顶端元素
bool empty() const { // stack 是否为空
return elems.empty();
}
};
template <typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem); // 追加(附于尾)
}
Stack<int> intStack;
intStack.push(7);
Stack<int>将类模板中的T替换成int,Stack内部使用可容纳int作为元素的vector
唯有被调用到的成员函数,才会实例化,这样做可以节省时间和空间;另一方面,可以实例一个类模板,并且实例化的类不需要完整支持模板类中与该类有关的所有操作
比如,一些自定义类没有实现
operator <,类模板中可能有一些函数中有比较大小的操作,那么只要不执行这些函数,以自定义类为T的类模板就不会报错
可以用模板实参来特化类模板,与函数模板的重载类似,通过特化类模板可以是实现基于某种特定类型的实现,或者克服某种特定类型在实例化类模板时所出现的不足
如果要特化类模板,需要特化该类所有成员函数,虽然也可以指特化某个成员函数,但是这个作为没有特化整个类,也就没有特化类模板
template<>
class Stack<std::string> {
private:
std::deque<std::string> elems; // 元素
public:
void push(std::string const&); // push 元素
void pop(); // pop 元素
std::string top() const; // 传回 stack 最顶端元素
bool empty() const { // stack 是否为空
return elems.empty();
}
};
void Stack<std::string>::push (std::string const& elem)
{
elems.push_back(elem); // 追加元素
}
这里使用deque来替代vector管理Stack内部元素,说明了特化的实现可以和基本类模板的实现完全不同
类模板可以被局部特化,在特定的环境下指定类模板的特定实现,并且要求某些模板参数仍然必须有用户类定义
template <typename T1, typename T2>
class MyClass {
// ...
};
可以存在下面两种偏特化
template <typename T>
class MyClass<T,T> {
// ...
};
// 偏特化:第二个类型为 int
template <typename T>
class MyClass<T,int> {
// ...
};
// 偏特化 两个template parameter均为指针
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>
MyClass<int*,int*> m; // 错误:同时匹配 MyClass<T,T> 和 MyClass<T1*,T2*>
可以为模板参数定义缺省值,这些值就被称为缺省模板参数,而且他们还可以引用之前的模板参数
template <typename T, typename CONT = std::vector<T>>
class Stack {
private:
CONT elems; // 元素
public:
void push(T const&); // push 元素
void pop(); // pop 元素
T top() const; // 传回最顶端元素
bool empty() const { // stack 是否为空
return elems.empty();
}
};
template <typename T, typename CONT>
void Stack<T,CONT>::push (T const& elem)
{
elems.push_back(elem); // 追加元素
}
可以像之前单一模板参数一样使用Stack<int>,也可以指定容器类型Stack<double, std::deque<double>>
对于钼函数模板和类模板,模板参数并不限于类型,普通值也可以作为模板参数
template <typename T, int MAXSIZE>
class Stack {
private:
T elems[MAXSIZE]; // 元素
int numElems{0}; // 当前的元素个数
public:
Stack(); // 构造函数
void push(T const&); // push 元素
void pop(); // pop 元素
T top() const; // 传回 stack 顶端元素
bool empty() const { // stack 是否为空
return numElems == 0;
}
bool full() const { // stack 是否已满
return numElems == MAXSIZE;
}
};
Stack<int, 100> stack1;
Stack<int, 200> stack2;
这里MAXSIZE是新加入的第二个模板参数,类型为int,指定了数组最多可包含的栈元素
但是这里的stack1和stack2属于不同的类型,而且这两种类型之间也不存在隐式或者显示的类型转换,也不能互相赋值
也可以为函数模板定义非类型参数
template<typename T, int Val>
T addValue(T const &x){
return x + Val;
}
int p = addValue<int, 10>(1);
非类型模板参数是有限制的,通常可以是常整数(包括枚举)或者指向外部链接对象的指针
浮点数和类型对象是不允许作为非类型模板参数的
template<double Val>
double process(double v){
return v * Val;
}
template<std::string name>
class MyClass{
// ...
};
上述都是错误的使用方法
之所以不能使用浮点数(包括简单的常量浮点表达式)作为模板参数有历史原因存在(未来可能支持)
std::string由于字符串文字是内部链接对象,所以不能使用它们来作为模板参数
template<char const* name>
class MyClass{
// ...
};
extern char const s[] = "hello";
MyClass<s> x; // ok
全局字符数组s由hello初始化,并且是extern的外部链接对象
模板中引入typename是为了说明:模板内部的标识符可以是一个类型
template <typename T>
class MyClass{
typename T::SubType *ptr;
// ...
}
此处使用typename被用来说明:SubType是定义域类T内部的一种类型,因此ptr是指向T::SubType类型的指针
如果不使用typename,那么SubType会被认为是一个静态成员,那么T::SubType * ptr会被认为是T::SubType和ptr的乘积
通常而言,当某个依赖于模板参数的名称是一个类型时,就应该使用typename
template<int N>
void printBitset(std::bitset<N> const& bs) // success
{
std::cout << bs.template to_string<char>();
}
template<int N>
void printBitset(std::bitset<N> const& bs) // error
{
std::cout << bs.to_string<char>();
}
这里出现了一个东西.template,这个东西一般用在模板函数中,它表示后面对象调用的另一个模板函数to_string<char>()中的<不是小于号,而是模板实参列表的起始符号
只有当.或->之前的对象构建去角色某个模板参数列表时,才需要注意这个问题(该问题在VS2019 C++14上并未出现,但是在旧版本和GCC上出现)
对于具有基类的类模板,自身使用名称x并不一定等同于this->x
template<typename T>
class Base{
public:
void BaseFunc(){
std::cout << "Base" << std::endl;
}
};
template<typename T>
class Derived : Base<T>{
public:
void foo(){
BaseFunc();
}
};
Derived<int> PP;
PP.foo(); // Error: “BaseFunc”: 找不到标识符 EmptyCpp
对于那些在基类中声明,并且依赖于模板参数的符号(函数或者变量等),都应该在其之前使用this->或者Base<T>::限定
类成员也可以是模板。嵌套类和成员你函数都可以作为模板
Stack<float> x, y;
Stack<int> z;
x = y; // OK 类型相同
x = z; // Error 类型不同
也可以通过定义一个身为模板的赋值运算符
template <typename T>
class Stack {
private:
std::deque<T> elems; // 元素
public:
template<typename T2>
Stack<T>& operator = (Stack<T2> const&);
};
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(Stack<T2> const& Other)
{
if ((void*)this == (void*)&Other) { // 判断是否赋值给自己
return *this;
}
Stack<T2> tmp(Other); // 建立 Other 的一份拷贝
elems.clear(); // 移除所有现有元素
while (!tmp.elems.empty()) { // 复制所有元素
elems.push_back(tmp.elems.front());
tmp.elems.pop_front();
}
return *this;
}
这里代码没啥大问题,只一点在operator=中,无法访问Other的elems属性,因为它是私有的。因为Stack<int>和Stack<flaot>是两种不同的类型
但是,如果是相同类型,因为自己是自己的友元,所以类型相同时是可以访问彼此的私有成员属性的
这里可以把elems改为public,或者提供对外访问接口
template <typename T, typename CONT = std::vector<T>>
class Stack {
private:
CONT elems; // 元素
public:
void push(T const&); // push 元素
void pop(); // pop 元素
T top() const; // 传回最顶端元素
bool empty() const { // stack 是否为空
return elems.empty();
}
};
Stack<int, std::vector<int>> S1; // Success
Stack<int, std::vector> S2; // Error
如果想要使用一个和缺省值不同的内部容器,必须两次指定元素类型
然而借助模板的模板参数,就可以只指定容器的类型,不用指定容器所含元素类型
template <typename T,
template <typename ELEM> class CONT = std::deque>
class Stack_V {
private:
CONT<T> elems; // 元素
public:
void push(const T& val);
bool empty() const { // stack 是否为空
return elems.empty();
}
};
template <typename T,
template <typename> class CONT>
void Stack_V<T, CONT>::push(const T& val)
{
elems.push_back(val);
}
不同之处在于第二个模板参数被声明为一个模板template <typename ELEM> class CONT,缺省值也从std::vector<T>变为std::vector,定义从CONT elems变为CONT<T> elems
作为模板参数的声明时,通常可以用typename来替换关键字class,但是这里不行,因为这里CONT时为了定义一个类,因此只能使用class
又因为template <typename ELEM> class CONT中的ELEM一般来说并不会用到,所以可以省略写法
template <typename T,
template <typename> class CONT = std::deque>
class Stack_V {
// ...
};
然后就是,函数模板并不支持模板的模板参数
对于int、double或者指针等基本类型,并不存在用一个有用的缺省值对他们进行初始化的缺省构造函数,相反任何未被初始化的局部变量都有一个不确定值
void foo(){
int x; // 不确定值
int* p; // p 指向某块未知内存
}
那么在模板中,一般来说都希望模板类型的变量都可以使用缺省值初始化,可是内建类型并不能满足需求
template<typename T>
void foo(){
T x; // x如果是内建类型,则x本身就是个不确定值
}
所以应该显示的调用内建类型的缺省构造函数,并把缺省值设置为0
比如
int()就是0
template<typename T>
void foo(){
T x = T();
}
对于模板类,在用某种类型实例化该模板后,为了确定所有的成员都已经初始化完毕,需要定义一个缺省构造函数
template<typename T>
class MyClass{
private:
T x;
public:
MyClass() : x(){
}
}
template <typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
std::string s = "peach";
::max("peach", "apple"); // OK:类型相同
::max("tomato", "apple"); // ERROR:类型不同
::max("apple", s); // ERROR:类型不同
"peach"是const char[6],"apple"是const char[6],"tomato"是const char[7],所以很明显的类型不同报错
但是如果声明的不是引用参数
template <typename T>
inline T const& max(T const a, T const b)
{
return a < b ? b : a;
}
std::string s = "peach";
::max("peach", "apple"); // OK:类型相同
::max("tomato", "apple"); // OK:退化为相同的类型
::max("apple", s); // ERROR:类型不同
对于非引用类型的参数,在实参演绎的过程中,会出现数组到指针(array-to-pointer)的类型转换
template<typename T>
void ref(const T& x) {
std::cout << "x is ref " << typeid(x).name() << std::endl;
}
template<typename T>
void unref(T x) {
std::cout << "x is unref " << typeid(x).name() << std::endl;
}
ref("hello");
unref("hello");
x is ref char const [6]
x is unref char const *
如果遇到关于字符数组和字符串指针之间不匹配的问题,可以往这方面考虑,根据实际情况有不同的解决方法
template<typename T, int N, int M>
T const* max(T const(&a)[N], T const(&b)[M]) {
return a < b ? b : a;
}
绝大多数的程序员这样组织代码结构
但是,模板不行,当模板的声明和定义不在同一个文件中,会触发链接错误
因为调用模板时,模板的定义还没被实例化,为了使模板真正被实例化,编译器必须知道应该实例化哪个定义以及要基于哪个模板实参来进行实例化,可是这两部分信息位于分开编译的不同文件里面
对于上面的问题,通常采取对待宏或内联函数的解决方法
#pragma once
// 声明
template<typename T>
void print_typeof(T const&);
// 实现
template<typename T>
void print_typeof(T const& x){
std::cout << typeif(x).name() << std::endl;
}
将上面的这种组织方式称为包含模型
包含模型明显增加了包含头文件的开销,这也是包含模型最大的不足之处
template<typename T1, typename T2> // 基本的类模板
class MyClass{
// ...
}
template<> // 显式特化
class MyClass<std::string, float>{
// ...
}
template<typename T> // 局部特化
class MyClass<T, T> {
// ...
}
class C; // 类C的声明
void f(int p); // 函数f的声明
extern int v; // 变量v的声明
template<typename T, int N>
class ArrayInClass{
public:
T array[N];
};
ArrayInClass<int, 10> temp;
C++支持两种基本类型的模板:类模板和函数模板
template<typename T>
class MyList { // 命名空间内的类模板
public:
template<typename T2> // 成员函数模板
MyList(MyList<T2> const&); // 构造函数
// ...
};
template<typename T>
template<typename T2>
MyList<T>::MyList(MyList<T2> const&) // 位于类外部的成员
{ // 函数模板的定义
}
template<typename T>
int Length(MyList<T> const&); // 位于外部命名空间作用域的函数模板
class Collection {
template<typename T> // 位于类内部的成员类模板
class Node {
// ...
};
template<typename T> // 另一个作为成员的类模板
class Handle;
template<typename T> // 位于类内部的成员函数模板的定义
T* allco() { // 显式内联函数
// ...
}
};
template<typename T> // 类外部定义的成员类模板
class Collection::Handle {
// ...
};
除了上面的,联合模板也是允许的(通常被当作类模板)
template<typename T>
union AllocChank{
T object;
unsigned char bytes[sizeof(T)];
}
和普通函数一样,函数模板声明也可以具有缺省调用实参
template<typename T>
void report_top(Stack<T> const&, int number = 10);
template<typename T>
void fill(Array<T>*, T cosnt & = T())
不过使用T()去初始化缺省调用实参也可能存在问题,比如下面的代码
class Value{
public:
Value(int);
}
void init(Array<Value> *arr){
Value zero(0);
fill(arr, zero); // 正确的
fill(arr); // 错误的 因为不存在Value()的无参构造函数
}
除了上述两种基本类型的模板之外,还可以使用相似的符号来参数化其他的3种声明
template <int I>
class CupBoard {
void open(); //译注:隶属于 CupBoard class template
class Shelf; //译注:隶属于 CupBoard class template
static double total_weight; //译注:隶属于 CupBoard class template
// ...
};
template <int I>
void CupBoard<I>::open()
{
// ...
}
template <int I>
class CupBoard<I>::Shelf {
// ...
};
template <int I>
double CupBoard<I>::total_weight = 0.0;
成员函数不能被声明为虚函数,因为虚函数调用机制的普遍实现都使用了一个大小固定的表,每个虚函数都对对应表的一个入口
但是,成员函数模板的实例化个数,要等到整个程序都翻译完毕才能确定,这就和表的大小(固定的)发生了冲突
不过,类模板的普通成员函数可以是虚函数,因为当类被实例化之后,他们的个数是固定的
template<typename T>
class TestClass
{
public:
virtual ~TestClass(); // Success
template<typename T2>
virtual void Copy(const T2&); // Error : 成员函数模板不能是虚拟的
};
3种模板参数
类型参数是通过关键字typename或者class引入的,关键字后面必须是一个简单的标识符,使用逗号隔开多个标识符,等号(=)表示缺省模板实参
在模板声明内部,类型参数的作用类似于typedef。如果T是一个模板参数,就不能使用注入class T等形式的修饰名称,即使T是一个要被class类型替换的参数也不可以
template<typename Allocator>
class List{
class Allocator* allocator; // Error
friend class Allocator; // Error
};
非类型参数表示的是 在编译器或链接期可以确定的常值
整个常值必须是
所有其他的类型都不允许作为非类型参数使用
template<typename T, // 类型参数
typename T::Allocator* Allocator> // 非类型参数
template<int buf[5]>
class Lexer;
// 上面等价于下面
template<int *buf>
class Lexer;
template<int const length>
class Buffer;
// 上面等同于下面 const是无效的
template<int length>
class Buffer;
非类型模板参数只能是右值,不能被取地址,不能被赋值
模板的模板参数是代表类模板的占位符,不能使用关键字class或union
template<template<typename X> class C> // Success
void f(C<int>* p);
template<template<typename X> struct C> // Error
void f(C<int>* p);
template<template<typename X> union C> // Error
void f(C<int>* p);
模板的模板参数的参数可以具有缺省模板实参
template<template<typename T,
typename A = MyLoocator> class Container>
class Adaptation{
Container<int> storage; // 隐式等同于 Container<int, MyLoocator>
// ...
}
对于模板的模板参数而言,它的参数名称只能被自身其他参数的声明使用
template<template<typename T, T*> class Buf>
class Lexer{
static char storage[5];
Buf<char, &Lexer<Buf>::storage[0]> buf;
// ...
};
template<template<typename T, T*> class List>
class Node{
static T* storage; // Error 模板的模板参数的参数不能被用在这里
// ...
}
任何类型的模板参数都可以拥有一个缺省实参,只要该缺省实参能够匹配整个参数就可以。显然缺省实参不能依赖于自身的参数,但可以依赖于前面的参数
template <typename T, typename Allocator = allocator<T>>
class List;
与缺省的函数调用实参的约束一样,对于任意一个模板参数,只有在之后的模板参数都提供了缺省实参的情况下,才能具有缺省模板实参
template <typename T1, typename T2, typename T3, typename T4 = char, typename T5 = char> // Success
class MyClass1;
template <typename T1, typename T2, typename T3, typename T4 = char, typename T5> // Error
class MyClass1;
template <typename T1 = char, typename T2, typename T3, typename T4, typename T5> // Error
class MyClass1;
除此之外,缺省实参不能重复声明
template<typename T = void>
class Value;
template<typenaem T = void>
class Value; // 重复出现的缺省实参
模板实参是指:在实例化模板时,用来替换模板参数的值
函数模板实参
对于函数模板的模板实参,可以显式的指定它们,也或者借助模板的使用方式对他们进行实参演绎
template<typename T>
inline T const& max(const T& a, const T& b)
{
return a < b ? b : a;
}
int main(){
max<double> (1.0, 2.0); // 显式指定模板实参
max(1.0, 2.0); // 实参演绎
max<int>(1.0, 2.0); // 显式的
return 0;
}
但是某些模板实参永远也得不到演绎的机会,所以最好时把实参所对应的参数放在模板参数列表的开始处,从而可以显示的指定这些参数,而其他的参数仍然可以进行实参演绎
template<typename DstT, typename SrcT>
inline DstT implicit_cast(SrcT const& x){ // SrcT可以被演绎 但是DstT不可以
return x;
}
double val = implicit_cast<double>(1);
比如把
implicit_cast改为template<typename SrcT, typename DstT>就必须显式的指定两个模板实参
由于函数模板可以被重载,所以显式提供所有的实参并不足以表示每一个函数
template <typename Func, typename T>
void apply(Func func_ptr, T x)
{
func_ptr(x);
}
template <typename T> void single(T);
template <typename T> void multi(T);
template <typename T> void multi(T*);
void foo() {
apply(single<int>, 1); // success
apply(multi<int>, 1); // error T 或者 T* 都可以被匹配
}
另外,在函数模板种,显式的指定模板实参可能会试图构造一个无效的C++类型
template<typename T>
RT1 test(typename T::X const *);
template<typename T>
RT2 test(...);
表达式test<int>会让第一个函数模板毫无意义,因为int中没有名为X的类型,但是它可以匹配第二个函数模板。因此,表达式&test<int>能够表示一个唯一函数地址(第二个函数地址)
所以不能用int来替换第一个模板的参数,并不意味着错误,替换失败并非错误(substitution-failure-is-not-an-error, SFINAE)
typedef char RT1;
typedef struct { char a[2]; } RT2;
#define type_has_memeber_type_X(T) (sizeof(test<T>(0)) == 1)
template<typename T>
RT1 test(typename T::X const*) {
return 1;
}
template<typename T>
RT2 test(...) {
return 0;
}
void foo_temp() {
std::cout << type_has_memeber_type_X(int) << std::endl;
std::cout << type_has_memeber_type_X(TestClass) << std::endl;
}
于是,就可以在编译器检查给定类型T是否具备成员类型X
SFINAE原则保护的只是防止创建无效的类型,但不能保护试图计算无效的表达式
template<int I> void f(int (&)[24/(4-I)]);
template<int I> void f(int (&)[24/(4+I)]);
int main()
{
&f<4>; // ERROR:除数为 0
}
非类型实参
非类型模板实参是那些提花你非类型参数的值,必须是下面的某一种
template <typename T, T nontype_param>
class C;
C<int, 33>* c1; // 整数型(integer type)
int a;
C<int*, &a>* c2; // 外部变量的地址
void f();
void f(int);
C<void (*)(int), f>* c3; // 一个函数名称。重载解析规则在此选用 f(int)。& 会被隐寓加入。
class X {
public:
int n;
static bool b;
};
C<bool&, X::b>* c4; // static class members 都可接受
C<int X::*, &X::n>* c5; // pointer-to-member 常数亦可接受
template<typename T>
void templ_func();
C<void (), &templ_func<double> >* c6; // function template 具现体也是一种函数
模板实参的一个普遍约束是在程序创建的时候编译器或者链接器要能够确定实参的值
另外,有一些常值不能作为有效的非类型实参
友元类的声明不能是类定义,在引入模板之后,友元类声明的唯一变化只是:可以命名一个特定的类模板实例为友元
class TestClass
{
};
template<typename T>
class Node {
};
template<typename T>
class Tree {
friend TestClass; // OK
friend Node<T>; // OK
};
template<typename T>
class Tree {
friend TestClass; // Error
friend Node<T>; // Error
};
class TestClass
{
};
template<typename T>
class Node {
};
显然,如果要把类模板的实例声明为其他类或者类模板的友元,该类模板在声明的地方必须是可见的
友元函数
template <typename T1, typename T2>
void combine(T1, T2);
class Mixer {
friend void combine<>(int&, int&); // OK:T1 = int&, T2 = int&
friend void combine<int, int>(int, int); // OK:T1 = int, T2 = int
friend void combine<char>(char, int); // OK:T1 = char T2 = int
friend void combine<char>(char&, int); // ERROR:与 combine() template 不匹配
friend void combine<>(long, long) { ... } // ERROR:不能在此处定义函数
};
不能在友元声明中定义一个模板实例,所以命名一个实例的友元声明不是不行的