泛型与模板.md 20 KB

简单基础

函数模板

自变量推到

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. 解决方法:指定类型
max<double>(4, 7.2);
  1. 解决方法:不同参数不同类型
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,指定了数组最多可包含的栈元素

但是这里的stack1stack2属于不同的类型,而且这两种类型之间也不存在隐式或者显示的类型转换,也不能互相赋值

非类型的函数模板参数

也可以为函数模板定义非类型参数

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

模板中引入typename是为了说明:模板内部的标识符可以是一个类型

template <typename T>
class MyClass{
    typename T::SubType *ptr;
    // ...
}

此处使用typename被用来说明:SubType是定义域类T内部的一种类型,因此ptr是指向T::SubType类型的指针

如果不使用typename,那么SubType会被认为是一个静态成员,那么T::SubType * ptr会被认为是T::SubTypeptr的乘积

通常而言,当某个依赖于模板参数的名称是一个类型时,就应该使用typename

.template

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

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 *

如果遇到关于字符数组和字符串指针之间不匹配的问题,可以往这方面考虑,根据实际情况有不同的解决方法

  1. 使用非引用参数,取代引用参数(会出现无用的拷贝)
  2. 重载,编写接收引用参数和非引用参数的两个重载函数
  3. 对具体类型进行重载
  4. 强制要求程序员使用显示类型转换
  5. 重载数组类型
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. 将定义和声明写在一起
#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名称的类
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;
  • 模板参数是指:位于模板声明或定义内部,关键字template后面所列举的名称(比如上面的N和T)
  • 模板实参是指:用替换模板参数的各个对象(上面的int和10)

深入模板

深入基础

参数化声明

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()的无参构造函数
}