Bladeren bron

feat: template的基础

刘聪 3 jaren geleden
bovenliggende
commit
7f0307caeb
1 gewijzigde bestanden met toevoegingen van 708 en 0 verwijderingen
  1. 708 0
      cpp/泛型与模板.md

+ 708 - 0
cpp/泛型与模板.md

@@ -0,0 +1,708 @@
+# 简单基础
+
+## 函数模板
+
+## 自变量推到
+
+```cpp
+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,所以报错
+
+1. 解决方法:指定类型
+
+```cpp
+max<double>(4, 7.2);
+```
+
+2. 解决方法:不同参数不同类型
+
+```cpp
+template <typename T1, typename T2>
+inline T1 const& max(T1 const& a, T2 const& b)
+{
+	return a < b ? b : a;
+}
+```
+
+### 模板参数
+
+```cpp
+template<typename T>
+inline T max(T a, T b)
+```
+
+- 模板函数的两种参数
+  - `template<typename T>` 中T就是模板参数
+  - `max(T a, T b)`中的a、b就是调用参数
+
+```cpp
+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)`
+
+```cpp
+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是可以被自变量推导出来的
+
+### 重载函数模板
+
+```cpp
+// 传回两个 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;
+}
+```
+
+> 一般来说,不同的重载形式之间最好不存在 **绝对必要的差异**,各重载形式之间应该只存在 **参数个数不同** 或 **参数类型的明确不同**,所以不要出现一个参数是引用、另一个不是引用的情况
+
+## 类模板
+
+### 声明
+
+声名类模板
+
+```cpp
+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);          // 追加(附于尾)
+}
+```
+
+### 使用类模板
+
+```cpp
+Stack<int> intStack;
+intStack.push(7);
+```
+
+`Stack<int>`将类模板中的T替换成int,Stack内部使用可容纳int作为元素的vector
+
+唯有被调用到的成员函数,才会实例化,这样做可以节省时间和空间;另一方面,可以实例一个类模板,并且实例化的类不需要完整支持模板类中与该类有关的所有操作
+
+> 比如,一些自定义类没有实现 `operator <`,类模板中可能有一些函数中有比较大小的操作,那么只要不执行这些函数,以自定义类为T的类模板就不会报错
+
+### 特化模板
+
+可以用模板实参来特化类模板,与函数模板的重载类似,通过特化类模板可以是实现**基于某种特定类型**的实现,或者克服某种特定类型在实例化类模板时所出现的不足
+
+> 如果要特化类模板,需要特化该类所有成员函数,虽然也可以指特化某个成员函数,但是这个作为没有特化整个类,也就没有特化类模板
+
+```cpp
+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`内部元素,说明了特化的实现可以和基本类模板的实现完全不同
+
+### 局部特化,偏特化
+
+类模板可以被局部特化,在特定的环境下指定类模板的特定实现,并且要求某些模板参数仍然必须有用户类定义
+
+```cpp
+template <typename T1, typename T2>
+class MyClass {
+    // ...
+}; 
+```
+
+可以存在下面两种偏特化
+
+```cpp
+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*> {
+    // ...
+}
+```
+
+各个模板的使用
+
+```cpp
+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*>
+```
+
+如果多个局部特化同等程度地匹配某个声明,那么存在二义性,会报错
+
+```cpp
+MyClass<int,int>    m;  // 错误:同时匹配 MyClass<T,T> 和 MyClass<T,int>
+MyClass<int*,int*>  m;  // 错误:同时匹配 MyClass<T,T> 和 MyClass<T1*,T2*>
+```
+
+### 缺省模板参数(预设模板自变量)
+
+可以为模板参数定义缺省值,这些值就被称为缺省模板参数,而且他们还可以引用之前的模板参数
+
+```cpp
+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>>` 
+
+## 非类型模板参数
+
+对于钼函数模板和类模板,模板参数并不限于类型,**普通值也可以作为模板参数**
+
+### 非类型的类模板参数
+
+```cpp
+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`属于不同的类型,而且这两种类型之间也不存在隐式或者显示的类型转换,也不能互相赋值
+
+### 非类型的函数模板参数
+
+也可以为函数模板定义非类型参数
+
+```cpp
+template<typename T, int Val>
+T addValue(T const &x){
+    return x + Val;
+}
+
+int p = addValue<int, 10>(1);
+```
+
+### 非类型模板参数的限制
+
+非类型模板参数是有限制的,通常可以是常整数(包括枚举)或者指向外部链接对象的指针
+
+**浮点数和类型对象是不允许作为非类型模板参数的**
+
+```cpp
+template<double Val>
+double process(double v){
+    return v * Val;
+}
+
+template<std::string name>
+class MyClass{
+    // ...
+};
+```
+
+> 上述都是错误的使用方法
+
+之所以不能使用浮点数(包括简单的常量浮点表达式)作为模板参数有历史原因存在(未来可能支持)
+
+`std::string`由于字符串文字是内部链接对象,所以不能使用它们来作为模板参数
+
+```cpp
+template<char const* name>
+class MyClass{
+    // ...
+};
+
+extern char const s[] = "hello";
+MyClass<s> x;   // ok
+```
+
+全局字符数组s由`hello`初始化,并且是extern的外部链接对象
+
+## 技巧性基础知识
+
+### typename
+
+模板中引入typename是为了说明:模板内部的标识符可以是一个类型
+
+```cpp
+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
+
+```cpp
+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上出现)
+
+### 使用 this->
+
+对于具有基类的类模板,自身使用名称`x`并不一定等同于`this->x`
+
+```cpp
+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>::`限定
+
+### 成员模板
+
+类成员也可以是模板。嵌套类和成员你函数都可以作为模板
+
+```cpp
+Stack<float> x, y;
+Stack<int> z;
+
+x = y;      // OK 类型相同
+x = z;      // Error 类型不同
+```
+
+也可以通过定义一个身为模板的赋值运算符
+
+```cpp
+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,或者提供对外访问接口
+
+### 模板的模板参数
+
+```cpp
+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
+```
+
+如果想要使用一个和缺省值不同的内部容器,必须两次指定元素类型
+
+然而借助**模板的模板参数**,就可以只指定容器的类型,不用指定容器所含元素类型
+
+```cpp
+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一般来说并不会用到,所以可以省略写法
+
+```cpp
+template <typename T,
+    template <typename> class CONT = std::deque>
+class Stack_V {
+    // ...
+};
+```
+
+然后就是,函数模板并**不支持模板的模板参数**
+
+### 零初始化
+
+对于int、double或者指针等基本类型,并不存在用一个有用的缺省值对他们进行初始化的缺省构造函数,相反任何未被初始化的局部变量都有一个不确定值
+
+```cpp
+void foo(){
+    int x;  // 不确定值
+    int* p; // p 指向某块未知内存
+}
+```
+
+那么在模板中,一般来说都希望模板类型的变量都可以使用缺省值初始化,可是内建类型并不能满足需求
+
+```cpp
+template<typename T>
+void foo(){
+    T x;    // x如果是内建类型,则x本身就是个不确定值
+}
+```
+
+所以应该显示的调用内建类型的缺省构造函数,并把缺省值设置为0
+
+> 比如`int()`就是0
+
+```cpp
+template<typename T>
+void foo(){
+    T x = T();
+}
+```
+
+对于模板类,在用某种类型实例化该模板后,为了确定所有的成员都已经初始化完毕,需要定义一个缺省构造函数
+
+```cpp
+template<typename T>
+class MyClass{
+    private:
+        T x;
+    public:
+        MyClass() : x(){
+
+        }
+}
+```
+
+### 使用字符串作为函数模板的实参
+
+```cpp
+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]`,所以很明显的类型不同报错
+
+但是如果声明的不是**引用参数**
+
+```cpp
+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)的类型转换
+
+```cpp
+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 *
+
+如果遇到关于字符数组和字符串指针之间不匹配的问题,可以往这方面考虑,根据实际情况有不同的解决方法
+
+1. 使用非引用参数,取代引用参数(会出现无用的拷贝)
+2. 重载,编写接收引用参数和非引用参数的两个重载函数
+3. 对具体类型进行重载
+4. 强制要求程序员使用显示类型转换
+5. 重载数组类型
+
+```cpp
+template<typename T, int N, int M>
+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<typename T>
+void print_typeof(T const&);
+
+// 实现
+template<typename T>
+void print_typeof(T const& x){
+    std::cout << typeif(x).name() << std::endl;
+}
+
+```
+
+> 将上面的这种组织方式称为**包含模型**
+
+包含模型明显增加了包含头文件的开销,这也是包含模型最大的不足之处
+
+### 显式实例化
+
+### 分离模型
+
+## 一些术语
+
+- 类模板:该类是一个模板
+- 模板类(通常由下面三种含义)
+  - 作为类模板的同义词
+  - 从模板产生的类
+  - 具有一个template-id名称的类
+
+```cpp
+template<typename T1, typename T2>  // 基本的类模板
+class MyClass{      
+    // ...
+}
+
+template<>                          // 显式特化
+class MyClass<std::string, float>{
+    // ...
+}
+
+template<typename T>                // 局部特化
+class MyClass<T, T> {
+    // ...
+}
+```
+
+```cpp
+class C;            // 类C的声明
+void f(int p);      // 函数f的声明
+extern int v;       // 变量v的声明
+```
+
+```cpp
+template<typename T, int N>
+class ArrayInClass{
+    public:
+        T array[N];
+};
+
+ArrayInClass<int, 10> temp;
+```
+
+- 模板参数是指:位于模板声明或定义内部,关键字template后面所列举的名称(比如上面的N和T)
+- 模板实参是指:用替换模板参数的各个对象(上面的int和10)
+
+# 深入模板
+
+## 深入基础
+
+### 参数化声明
+