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