模板与泛型


一、函数模板

定义一个函数模板:

template<typename T>  // 定义函数模板
T funadd(T a, T b) {
	T addhe = a + b;
	return addhe;
}

尖括号里面是模板参数列表,如果这里的模板参数有多个,则用逗号分开,尖括号里至少要有一个模板参数。模板参数列表里面表示在函数定义中用到的“类型”或者“值”,使用的时候有时得指定模板实参,指定的时候也得用<>把模板实参包起来,有时又不需要指定模板实参,系统能够根据一些信息推断出来。函数模板调用的时候,先不用看函数模板定义中template<>这里有多少个模板参数,看的还是函数模板定义里面的函数名后面的参数数量

int main() {
    {
        int he = funadd(3, 1);
    }
    {
        float he = funadd(3.1f, 1.2f);
    }
    {
        // 下面这行编译出错,不知道模板参数类型应该推断为int类型还是float类型
        // float he = funadd(3, 1.2f);
    }
    return 0;
}			

看一看上述案例的模板参数列表template<typename T>,这里的T,因为前面是用typename来修饰,所以T代表一个类型,是类型参数。在这个模板参数列表里,还可以定义非类型参数。非类型参数表示的是一个值,不能用typename/class来修饰,例如非类型参数s是一个整型,那就写成int s

template<int a, int b>  // 定义函数模板
int funcaddv2() {
	int addhe = a + b;
	return addhe;
}

这里没有类型模板参数,只有非类型模板参数。调用这个函数模板:

int main() {
    {
        // 要通过<>来传递参数,就得看模板函数的<>里有几个参数
        // 这种<>写法就是显式指定模板参数,在尖括号中提供额外信息
        int result = funcaddv2<12, 13>();
        cout << result << endl;  // 25
    }
    {
        int a = 12;
        // 这不可以,非类型模板参数必须是常量表达式,值必须是在编译的时候就能确定
        // 因为实例化模板是在编译的时候做的事
        // int result = funcaddv2<a, 14>();
	}
    return 0;
}

再定义一个函数模板:

template<typename T, int a, int b>
int funcaddv3(T c) {
	int addhe = (int)c + a + b;
	return addhe;
}

int main() {
    {
        // 类型参数为int,实参为13
        int result = funcaddv3<int, 11, 12>(13);
        cout << result << endl;  // 36
    }
    {
        // 类型参数为double,实参为13,系统会以"<>"传递进去的类型为准
        // 而不是以13推断出来的类型为准
        int result = funcaddv3<double, 11, 12>(13);
        cout << result << endl;  // 36
    }
    return 0;
}

再定义一个函数模板:

template<unsigned L1, unsigned L2>  // 本例依旧没有类型参数
int charscomp(const char(&p1)[L1], const char(&p2)[L2]) {
	return strcmp(p1, p2);
}

int main() {
    {
        // 根据test2能推断出大小是6个(算末尾的\0)取代L1,L2同理,推断出大小是5个	
        int result = charscomp("test2", "test");
        cout << result << endl;  // 1
    }
    return 0;
}

上面针对charscomp()的调用,编译器实例化出来的版本是:

int charscomp(const char(&p1)[6], const char(&p2)[5]) {...}

再次提醒,非类型模板参数必须是一个常量表达式,否则编译会出错。函数模板也可以写成inlineinline的位置放在模板参数列表之后:

template<unsigned L1, unsigned L2>
inline int charscomp(const char(&p1)[L1], const char(&p2)[L2]) {
    return strcmp(p1, p2);
}

函数模板的定义并不会导致编译器生成相关代码,只有调用这个函数模板时,编译器才会实例化一个特定版本的函数并生成函数相关代码。总结一下模板参数:
模板参数分类总结

二、类模板

编译器不能为类模板推断模板参数,所以使用类模板必须在模板名后面用尖括号<>提供额外信息,这些信息其实就是对应着模板参数列表里的参数。例如vector<int>这里面的vector是类模板,尖括号里的int就理解成模板参数,通过这个模板参数指出容器vector中所保存的元素类型。类模板(也称模板类)定义的一般形式如下:

template<typename 形参名1, typename 形参名2, ..., typename 形参名n>
class 类名 {
    // ......
};

对于类模板,因为实例化具体类的时候必须有类模板的全部信息,包括类模板中成员函数的函数体具体内容等,所以类模板的所有信息,不管是声明还是实现等内容,都必须写到一个.h文件中去,其他的要用到类模板的源程序文件(如. cpp文件)只要#include这个类模板的.h文件即可,例如在文件myvector.h中定义:

#ifndef __MYVECTOR__
#define __MYVECTOR__

// 自己的容器类模板
template<typename T>  // 名字为T的模板参数,用来表示myvector这个容器所保存的元素类型
class myvector {
   public:
    typedef  T* myiterator;  // 迭代器
   public:
    // 构造函数
    myvector();
    // 赋值运算符重载,在类模板内部使用模板名myvector并不需要提供模板参数,当然提供也行,可以写成myvector<T>
    myvector& operator=(const myvector&);

   public:
	// 迭代器接口
    myiterator mybegin();  // 迭代器起始位置
    myiterator myend();    // 迭代器结束位置
};
#endif

实例化这个类模板:

#include "myvector.h"
int main() {
    {
        myvector<int>  tmpvec;     // T被替换成了int
        myvector<double> tmpvec2;  // T被替换成了double
        myvector<string> tmpvec3;  // T被替换成了string
    }
    return 0;
}

myvector是类模板名,myvector<int>等才是真正的类型名(实例化了的类模板)。在上述类模板中,增加成员函数(定义和声明写在一起):

   public:
    void myfunc() {};

类模板一旦被实例化之后,这个类模板的每个实例都会有自己版本的成员函数。所以,类模板的成员函数具有和这个类模板相同的模板参数(这句话的核心意思是:类模板的成员函数是有模板参数的)。如果这个类模板的成员函数定义在类模板里面,那么这个成员函数的模板参数体现不出来,但假如把类模板的成员函数的实现写在类模板定义的外面,那么这个成员函数的模板参数就体现出来了。也就是说,定义在类模板之外的成员函数必须以关健字template开始,后面接类模板参数列表。同时在类名后面要用尖括号<>把模板参数列表里面的所有模板参数名列出来,如果是多个模板参数则用,分隔。

   public:
    void myfunc();  // 函数声明

在类模板外部定义:

template<typename T>
void myvector<T>::myfunc() {}

再写一下构造函数的实现:

template<typename T>
myvector<T>::myvector() {}

一个类模板虽然里面可能有很多成员函数,但是当实例化模板之后,如果后续没有使用到某个成员函数,则这个成员函数是不会被实例化的。再看一下类模板名字的使用,在上述类模板中有一个赋值运算符的重载代码:

myvector& operator=(const myvector&);

赋值运算符重载返回一个myvector的引用,在类模板内部可以直接使用类模板名,并不需要在类模板名后跟模板参数,当然非要在类模板名后面跟模板参数也可以:

myvector<T>& operator=(const myvector<T>&);  // 赋值运算符重载

如果在类模板定义之外实现:

template<typename T>
// 第一个<T>表示返回的是一个实例化了的myvector,第三个<T>不是必加
myvector<T>& myvector<T>::operator=(const myvector<T>&) {
	//......
	return *this;
}

模板参数并不局限于类型,普通的值也能作为模板参数,也就是非类型模板参数。前面讲函数模板有非类型模板参数,而myvector类模板中是一个类型模板参数。创建一个新的类模板来演示非类型模板参数,创建一个新的文件myarray.h:

#ifndef __MYARRAY__
#define __MYARRAY__
template<typename T, int size = 10>
class myarray {
   private:
    T arr[size];
};
#endif

存在非类型模板参数size,而且还有默认值。

#include "myarray.h"

int main() {
    {
        myarray<int, 100> tmparr;
    }
    {
        myarray<int> tmparr;
    }
    return 0;
}

在类模板增加一个成员函数的声明:

   public:
    void myfunc();

在类模板外面实现:

template<typename T, int size>
void myarray<T, size>::myfunc() {
	std::cout << size << std::endl;
	return;
}

在main函数中调用:

myarray<int> tmparr;
tmparr.myfunc();  // 10

myarray<int, 50> tmparr2;
tmparr2.myfunc();  // 50

注意,非类型的模板参数,参数的类型有一定的限制:
(1)浮点型一般不能作为非类型模板参数:

template<typename T, double size>
class myarray {...};

(2)类类型也不能作为非类型模板参数:

class a {};

template<typename T, a size>
class myarray {...};

三、typename 的使用、函数模板与默认模板参数

1、typename 的使用场合

(1)在模板定义里,表明其后的模板参数是类型参数,看一个函数模板的定义:

template<typename T, int a, int b>
int funcaddv2(T c) {...}

再看一个类模板的定义:

template<typename T>  // 名字为T的模板参数,表示myvector容器所保存的元素类型
class myvector {...};

这里,typename 也可以写成 class,这两个关键字在这里功能一样。但需要注意的是,这里如果写成 class,和类定义时用的 class 完全是两个不同的含义。

(2)用 typename 标明这是一个类型(类型成员):“::”是作用域运算符,当访问类中的静态成员变量时需要用到,即类名::静态成员变量名。例如,定义一个类的静态成员变量并赋值:

int Time::mystatic = 5;

另外,“::”还可以用来标明类型成员。把上面代码中的 mybegin 成员函数(迭代器接口)的实现放到类模板 myvector 定义之外:

template<typename T>
typename myvector<T>::myiterator myvector<T>::mybegin() {...}

上面的代码首先要强调作用域运算符“::”的第二个用法一一访问类型成员。myiterator 是一个类型(因为它是用 typedef 定义出来的),mybegin 成员函数返回的正好是这种类型。所以在类模板定义之外要书写这种类型就要用到“::”,就是上面看到的这种myvector<T>::myiterator 写法来代表一个类型。
myvector<T>::myiterator之前,额外用到了一个typename,因为类模板中有个模板参数 T,这个 T 可能是任意一种类型,编译器在遇到这种 T:: 后面跟着一些内容的时候会导致编译器无法区分“::”之后的内容(也就是这里的 myvector<T>::myiterator)到底表示一个类型还是表示一个静态成员变量,直到遇到实例化这个类模板的实例化代码时才会确定。但是编译器在处理这个类模板时还必须要知道这个 myiterator 到底是一个静态数据成员变量名还是一个类型。而默认情况下,C++假定通过作用域运算符访问的是静态成员变量而不是类型,所以这里如果不加 typename 来修饰,编译会给出一个警告和一些错误。这个警告是:“myiterator” 依赖名称不是类型,解决办法就是显式地告诉编译器 myiterator 是一个类型,所以在其前面用 typename 来修饰。注意这里的typename 不能用 class 来替换。

再看一个范例,写一个函数模板:

template <typename T>
typename T::size_type getlength(const T& c) {  // 前面要加typename,否则报错
    if (c.empty()) {  // 这些诸如 empty、size 等成员函数,在函数模板未被实例化之前,谁也不知道
                      // 这些成员函数到底写得对还是不对,但一旦被实例化,自然可知
        return 0;
    }
    return c.size();
}

在 main 函数加入以下代码:

string mytest = "I love China!";
// size_type 类似 unsigned int 类型,定义于 string 类中,一般和 string 配套使用,考虑到各种机器
// 中数据类型的差异,所以引入这个类型,保证程序与实际的机器匹配
// string::size_type size = mytest.size();
string::size_type size2 = getlength(mytest);
cout << size2 << endl;  // 13

2、函数指针作为其他函数的参数

有下面这样一个函数:

int mf(int tmp1, int tmp2) {
    // ......
    return 1;
}

假如现在要把函数指针作为某个函数的参数进行传递,在文件开头定义一个函数指针类型:

typedef int(*FunType)(int, int);  // 可以在一个头文件中定义一个函数指针类型和函数本身的参数,
                                  // 返回值类型都一致,这里定义在 cpp 文件开头就可以

现在来定义一个函数:

void testfunc(int i, int j, FunType funcpoint) {  // 最后一个参数为函数指针类型
    // 可以通过函数指针调用函数
    int result = funcpoint(i, j);  // 这个就是通过函数指针调用函数
    cout << result << endl;
}

在 main 函数中加入以下代码:

testfunc(3, 4, mf);  // 调用testfunc,其中第三个参数为另外一个函数的函数名,函数名被作为函
                     // 数首地址可以传递到函数 testfunc 的第三个参数里,而 testfunc 的第
                     // 三个参数正好是函数指针(函数指针代表函数首地址)

3、函数模板趣味用法

把上面的 testfunc 函数改写成函数模板:

template <typename T, typename F>
void testfunc(const T& i, const T& j, F funcpoint) {
    cout << funcpoint(i, j) << endl;
}

在 main 函数中调用:

testfunc(3, 4, mf);

结果打印出1。系统根据传入的参数推断出 T 是 int,F 是函数指针类型。现在引入“可调用对象”的概念:如果一个类重载了“()”运算符,那么如果生成了该类的一个对象,就可以用“对象名(参数…)”的方式来使用该对象,那么用这个类生成的对象就是一种可调用对象(可调用对象有很多种,这里先只说这一种)。现在写一个可调用对象所代表的类,只要重载“()”运算符即可。

class tc {
   public:
    tc() {cout << "构造函数执行" << endl;}
    tc(const tc& t) {cout << "拷贝构造函数执行" << endl;}
    // 重载圆括号
    int operator()(int v1, int v2) const {
        return v1 + v2;
    }
};

在 main 函数中加入以下代码:

tc tcobj;
testfunc(3, 4, tcobj);  // 这里调用拷贝构造函数

函数 testfunc 的第3个参数传递进去了一个 tcobj 对象,系统推断模板参数 F 的类型应该为 tc(类类型),因此 testfunc 函数模板这里会调用 tc 类的拷贝构造函数来生成一个叫作 funcpoint 的 tc 类型的对象。然后在 testfunc 这个函数模板中,代码行cout << funcpoint(i, j) << endl;实际执行的就是可调用对象(把类对象当成函数一样调用),所以这行代码打印出来的结果为 7。

现在在 main 函数中换一种写法:

testfunc(3, 4, tc());

调试发现上面代码行调用了 tc 类的构造函数,生成了一个 tc 类的对象(临时对象),直接传递到函数模板 testfunc 的 funcpoint 形参里面去了。这里并没有执行 tc 类的拷贝构造函数,只执行了一次 tc 类的构造函数。这说明系统推断 F 类型应该为 tc(类类型),然后直接把代码 tc() 生成的临时对象构造到 funcpoint 对象(形参)中去了,这样就节省了一次拷贝构造函数的调用,自然也就节省了一次析构函数的调用。

同一个函数模板 testfunc,根据传递进去的参数不同,就能够推断出不同的类型,上面的演示推断出了两种类型:

  • 推断出的是函数指针
  • 推断出的是一个对象,而且是一个可调用对象

这两种类型一个是“函数指针”,一个是“对象”,在这里共用同一个模板。tc 类对象必须是一个可调用对象,也就是 tc 类本身必须重载“()”运算符,并且这个运算符里面的参数和返回值类型必须要与函数模板里面进行函数或可调用对象调用时所需要的参数类型以及返回值类型匹配。

4、默认模板参数

(1)类模板必须有尖括号,尖括号表示类必须是从一个类模板实例化而来。如果一个类模板的模板参数有默认值:

template <typename T = string, int size = 5>
class myarray {......};

如果某个模板参数有默认值,那么从这个有默认值的模板参数开始,后面的所有模板参数都得有默认值(这一点和函数的形参默认值规则一样)。调用的时候,如果完全用默认值,则可以直接使用一个空的尖括号(空的尖括号不能省):

myarray<> abc;

如果想提供一个模板参数,而另外一个模板参数要用默认值,可以这样写代码:

myarray<int> def;

(2)函数模板也可以有默认模板参数(C++11):

template <typename T, typename F = tc>
void testfunc(const T &i, const T &j, F funcpoint = F()) {
    cout << funcpoint(i, j) << endl;
}

可以这样调用(最后一个模板参数并没有提供):

testfunc(3, 4);

上面的写法不但为模板参数 F 提供默认参数 tc(类名),还为函数模板 testfunc 的第三个形参提供了默认值,注意这个默认值的写法F funcpoint = F()。这个等于默认在这里构造(定义)了一个临时的类 tc 的对象,直接构造到 funcpoint 所代表的空间,现在funcpoint 就是一个类 tc 的对象。有几点说明:

  • 必须同时为模板参数和函数参数指定默认值,否则语法通不过且语义也不完整。
  • 这种产生一个临时对象作为默认值的写法,其实等价于如下代码:
void testfunc(const int &i, const int &j, tc funcpoint = tc()) {
    cout << funcpoint(i, j) << endl;
}
  • tc 类必须重载“()”运算符,也就是说必须保证 funcpoint 是一个可调用对象,否则代码行cout << funcpoint(i, j) << endl;编译时会报错。
  • 一旦给函数提供了正常参数,那默认参数就不起作用了。

函数指针类型也可以作为函数模板的默认模板参数:

template <typename T, typename F = FunType>
void testfunc(const T &i, const T &j, F funcpoint = mf) {
    cout << funcpoint(i, j) << endl;
}

默认模板参数 F 是一个函数指针类型(FunType),函数参数funcpoint = mf中的mf是函数名,代表函数首地址。

四、成员函数模板、模板显式实例化与声明

1、普通类的成员函数模板

不管是普通类还是类模板,它的成员函数本身可以是一个函数模板,这种成员函数称为“成员函数模板”。成员函数模板不可以是虚函数,否则编译器会报错。

class A {
   public:
    template<typename T>
    void myft(T tmpt) {
        cout << tmpt << endl;
    }
};

myft 就是成员函数模板,可以这样调用:

A a;
a.myft(3);  // 3

在调用 myft 成员函数模板时,编译器就会实例化这个成员函数模板。

2、类模板的成员函数模板

类模板和它的成员函数模板有各自独立的模板参数:

template<typename C>
class A {
   public:
    template<typename T2>
    A(T2 v1, T2 v2) {  // 构造函数也引入自己的模板参数T2,和整个类的模板参数C没有关系
    
    }

    template<typename T>
    void myft(T tmpt) {
        cout << tmpt << endl;
    }

    C m_ic;
};

在 main 函数中加入以下代码:

A<float> a(1, 2);  // 类模板的模板参数必须用<>指定,函数模板的模板参数可以推断
A<float> a2(1.1, 2.2);
a.myft(3);  // 3

类模板本身有自己的模板参数 C,而成员函数模板也有自己的模板参数 T2、T1,两者之间互不打扰。现在看一下把成员函数模板的实现代码写到类模板定义之外去:

template<typename C>   // 先跟类模板的模板参数列表
template<typename T2>  // 再跟构造函数模板自己的模板参数列表
A<C>::A(T2 v1, T2 v2) {
    cout << v1 << v2 << endl;
}

在 main 函数中调用:

A<float> a(1, 2);  // 实例化了一个A<float>类,并用int型来实例化构造函数
A<float> a2(1.1f, 2.2f);  // A<float>已经被上面代码行实例化过了,这里用float来实例化构造函数

请记住:①类模板中的成员函数,只有源程序代码中出现调用这些成员函数的代码时,这些成员函数才会出现在一个实例化了的类模板中。②类模板中的成员函数模板,只有源程序代码中出现调用这些成员函数模板的代码时,这些成员函数模板的具体实例才会出现在一个实例化了的类模板中。

3、模板显式实例化与声明

模板只有被使用时才会被实例化,但是有这样一个问题,先看下面这种情况,假设工程中有一个 ca.h 头文件,里面定义了一个类模板:

#ifndef __CAH_
#define __CAH_
template<typename C>
class A {
   public:
    template<typename T2>
    A(T2 v1, T2 v2);
    
    template<typename T>
    void myft(T tmpt) {
        std::cout << tmpt << std::endl;
    }
    C m_ic;
};

template<typename C>
template<typename T2>
A<C>::A(T2 v1, T2 v2) {
    std::cout << v1 << v2 << std::endl;
}
#endif

工程中还有两个.cpp文件,它们都包含 ca.h,内容分别是:

#include <iostream>
#include <vector>
#include "ca.h"
using namespace std;
void mfunc() {
    A<float> a(1, 2);
}
#include <iostream>
#include <vector>
#include "ca.h"
using namespace std;
int main() {
    A<float> a(1, 2);
    A<float> a2(1.1, 2.2);
    a.myft(3);  // 3
    return 0;
}

当这两个 .cpp 代码中的A<float> a(1, 2);这行代码在编译的时候,因为每个 .cpp 文件独立编译,所以编译器在两个 .cpp 中都会实例化出一个A<float>类。如果项目很大,.cpp 文件很多,那这个额外的开销会增加很多没有必要的编译时间。可以通过“显式实例化”来避免这种生成多个相同类模板实例的开销。在其中一个 .cpp 文件头写入如下代码:

template A<float>;  // 这叫"实例化定义”,只有一个.cpp文件里这样写,编译器为其生成代码

在其他的 .cpp 的头上声明这个实例化出来的类就行了:

extern template A<float>;  // 其他所有.cpp文件都这样写

函数模板也是一样,在 ca.h 中定义一个函数模板:

template<typename T>
void myfunc(T v1, T v2) {
    std::cout << v1 + v2 << std::endl;
}

然后在其中一个 .cpp 中的上面位置这样写:

template void myfunc(int &v1, int &v2);  // 函数模板实例化定义,编译器会为其生成实例化代码

在其他 .cpp 文件开头这样写:

extern template void myfunc(int &v1, int &v2);  // 函数模板实例化声明

当然,如果有一个 .cpp 文件使用了int作为模板参数的类模板,那这个int作为模板参数的类模板还会在这个 .cpp 中被实例化:

A<int> d(6, 7);  // int版本的A(A<int>)会被实例化

五、using 定义模板别名与显式指定模板参数

1、using 定义模板别名

前面知道 typedef 一般用来定义类型别名,例如:

typedef unsigned int uint_t;  // 相当于给unsigned int类型起了个别名uint_t

现在有这样一个类型std::map<std:string, int>,可以这样给它起个别名:

typedef std::map<std::string, int> map_s_i;

后面就可以这样来使用这种类型:

map_s_i mymap;
mymap.insert({"first", 1});

如果在实际开发中有这样一个需求:希望定义一个类型,但这个类型不固定,例如对于 map 类型容器中的元素,元素的键(key)固定是std::string类型,但值(value)不希望固定为int或者固定为string类型,这个需求通过typedef是很难办到的,因为typedef一般都是用来给固定类型起别名,而这里的类型名不固定,像一个模板一样。于是 C++98 标准那个时代,开发者就想了一个变通的办法来达到这个目的:通过一个类模板来实现。看一看代码:

template<typename wt>
struct map_s {
    typedef std::map<std::string, wt> type;  // 定义了一个类型
};

在 main 函数中增加以下代码:

map_s<int>::type map1;  // 等价于"std::map<std::string, int> map1;"
map1.insert({"first", 1});

而现在 C++11 用如下两行代码就能解决问题:

template<typename T>
using str_map_t = std::map<std::string, T>;

在 main 函数中增加以下代码:

str_map_t<int> map1;
map1.insert({"first", 1});

用 using 关键字给这个模板起了一个名字,这里叫 str_map_t。using 包含了 typedef 的所有功能,只不过语法上两者不太一样:

typedef unsigned int uint_t;
using uint_t = unsigned int;  // typedef 后的两个内容的位置反过来

再比较:

typedef std::map<std::string, int> map_s_i;
using map_s_i = std::map<std::string, int>;

可以看到,using 也可以定义普通类型,但在语法格式上正好和 typedef 定义类型的顺序相反。再看一下两者定义函数指针类型的区别:

typedef int(* FunType)(int, int);
using FunType = int(*)(int, int);

这里再看一个例子,看看 using 如何定义类型相关的模板(给函数指针类型模板起别名):

template<typedef T>
using myfunc_M = int(*)(T, T);

在 main 函数中增加以下代码:

myfunc_M<int> pointFunc;  // 函数指针,该函数返回一个int,参数是两个int,注意myfunc_M<int>是类型名(类型别名),并不是一个类模板实例化后的类

增加一个函数定义:

int RealFunc(int i, int j) {
    return 3;
}

在 main 函数中增加以下代码:

pointFunc = RealFunc;  // 把函数地址赋给函数指针
cout << pointFunc(1, 6) << endl;

2、显式指定模板参数

先写一个函数模板来求和,可以指定返回的结果类型从而控制显示的精度:

template<typename T1, typename T2, typename T3>
T1 sum(T2 i, T3 j) {
    T1 result = i + j;
    return result;
}

在 main 函数中增加以下代码:

auto result = sum(2000000000, 2000000000);  // 报错
cout << result << endl;

编译报错,提示“未找到匹配的重载函数”。T2 和 T3 类型可以通过调用 sum 函数时的实参推断出来,但是 T1 需要给进来一个模板参数才能推断:

auto result = sum<int>(2000000000, 2000000000);

另外两个模板参数 T2 和 T3 可以不提供,系统会自己判断。但是20亿加20亿,用 int 型会溢出,修改代码:

auto result = sum<double, double, double>(2000000000, 2000000000);

六、模板全特化与偏特化(局部特化)

1、类模板特化

先看类模板的全特化,看下面这个类模板的定义:

template<typename T, typename U>
struct TC {
    TC() {
        cout << "TC泛化版本构造函数" << endl;
    }
    void funtest() {
        cout << "TC泛化版本" << endl;
    }
};

TC 类模板属于一个泛化的类模板(以往实现的类模板也都属于泛化的类模板),得先有泛化版本才能有特化版本。现在有了泛化的类模板,怎样特化呢?例如要针对int, int类型做专门的处理,那这里的类型模板参数 T 和 U 就可以都用int类型代表,T 和 U 类型就不存在了(被绑定成一个具体类型了)。“全特化”就是所有类型模板参数都得用具体的类型代替。针对 T 和 U 都为int类型的特化版本要这样写(下面代码要放在 TC 类模板泛化版本代码的下面):

template<>  // 全特化所有类型模板参数都用具体类型代表,所以“<>”里就空了
struct TC<int, int> {  // 上面的T绑定到这里的第一个int,上面的U绑定到这里的第二个int
    TC() {
        cout << "TC<int, int>特化版本构造函数" << endl;
    }
    // 在这里可以对该特化版本做单独处理
    void functest() {
        cout << "TC<int, int>特化版本" << endl;
    }
};

如果没有这个特化的版本,那生成任何 TC 类模板的对象并调用functest成员函数,都应该执行泛化版本的functest()函数。但因为有了这个特化版本,那么如果使用 TC 类模板并指定了int, int类型,编译器就会执行这些特化版本的代码(特化版本代码具有优先被选择权)。

TC<char, int> tcchar;  // TC泛化版本构造函数
tcchar.functest();     // TC泛化版本
TC<int, int> tcint;    // TC<int, int>特化版本构造函数
tcint.functest();      // TC<int, int>特化版本

特化版本可以有任意多,目前只写了一个特化版本。再来看特化类模板的成员函数:

template<>  // 全特化
void TC<double, double>::functest() {
    cout << "TC<double, double>的functest()特化版本" << endl;
}

在 main 函数写入以下代码:

TC<double, double> tdbldbl;  // TC泛化版本构造函数
tdbldbl.functest();          // TC<double, double>的functest()特化版本

构造tdbldbl对象调用的是泛化版本的 TC 类模板的构造函数(因为 TC 类模板特化的只有int, int),但是专门为成员函数functest进行了一个double, double类型的特化,所以调用的是functest的特化版本。

接下来看类模板的偏特化(局部特化),从两个方面说,一个是模板参数数量上的偏特化,一个是模板参数范围上的偏特化。

template<typename T, typename U, typename W>
struct TCP {
    TCP() {
        cout << "TCP泛化版本构造函数" << endl;
    }
    void functest() {
        cout << "TCP泛化版本" << endl;
    }
};

上述 TCP 类模板有三个模板参数,如果特化其中两个,另一个不特化,就叫偏特化,代码可以这样写:

template<typename U>  // 另外两个模板参数被绑定了,这里剩一个
struct TCP<int, U, double> {
    TCP() {
        cout << "TCP<int, U, double>偏特化版本构造函数" << endl;
    }
    void functest() {
        cout << "TCP<int, U, double>偏特化版本" << endl;
    }
};

在 main 函数写入以下代码:

TCP<double, int, double> tcpdi;  // TCP泛化版本构造函数
tcpdi.functest();                // TCP泛化版本
TCP<int, int, double> tcpii;     // TCP<int, U, double>偏特化版本构造函数
tcpii.functest();                // TCP<int, U, double>偏特化版本

下面是模板参数范围上的偏特化。首先理解一下参数范围的概念,例如原来是int类型,如果变成const int类型,这个类型的范围上就变小了,再如,如果原来是任意类型 T,现在变成 T *(从任意类型缩小为指针类型),还有 T&(左值引用)、T&&(右值引用)针对 T 来说,从类型范围上都属于变小了。先写一个泛化的类模板(先有泛化才能有特化版本):

template<typename T>
struct TCF {
    TCF() {
        cout << "TCF泛化版本构造函数" << endl;
    }
    void funtest() {
        cout << "TCF泛化版本" << endl;
    }
};

下面是一个模板参数范围上的特化版本:

template<typename T>
struct TCF<const T> {  // const 特化版本
    TCF() {
        cout << "TCF<const T>特化版本构造函数" << endl;
    }
    void functest() {
        cout << "TCF<const T>特化版本" << endl;
    }
};

在 main 函数写入以下代码:

TCF<double> td;  // TCF泛化版本构造函数
td.functest();   // TCF泛化版本

TCF<const int> tcfi;  // TCF<const T>特化版本构造函数
tcfi.functest();      // TCF<const T>特化版本

所以即便是偏特化,特化完了它本质上还是一个模板。

2、函数模板特化

先来一个泛化版本的函数模板:

template<typename T, typename U>
void tfunc(T& tmprv, U& tmprv2) {
    cout << "tfunc泛化版本" << endl;
    cout << tmprv << endl;
    cout << tmprv2 << endl;
}

在 main 函数写入以下代码:

const char *p = "I love China!";
int i = 12;
tfunc(p, i);

运行结果:

tfunc泛化版本
I love China!
12

现在写一个全特化版本:

template<>  // 全特化<>里面就是空的
void tfunc(int& tmprv, double& tmprv2) {  // 格式要与泛化版本一一对应,否则编译会报错,例如第二个参数写成double tmprv2就会报错
    cout << "--- begin ---" << endl;
    cout << "tfunc<int, double>特化版本" << endl;
    cout << tmprv << endl;
    cout << tmprv2 << endl;
    cout << "--- end ---" << endl;
}

在 main 函数写入以下代码:

int k = 12;
double db = 15.8f;
tfunc(k, db);  // 这里调用的就是特化版本

运行结果:

--- begin ---
12
15.8
--- end ---

全特化等价于实例化一个函数模板,不等价于一个函数重载。注意比较下面两行代码:

void tfunc<int, double>(int& tmprv, double& tmprv2) {};  // 全特化长这样,等价于实例化一个函数模板
void tfunc(int tmprv, double tmprv2) {...}  // 重载的函数长这样

上面已经存在了int, double类型的函数模板tfunc全特化,如果再存在一个int, double类型形参的重载函数,代码如下:

void tfunc(int& tmprv, double& tmprv2) {
    cout << "--- begin ---" << endl;
    cout << "tfunc普通函数" << endl;
    cout << "--- end ---" << endl;
}

那么上面 main 函数中的代码行tfunc(k, db)就不会调用tfunc函数模板的特化版本,而是会调用tfunc重载函数。这说明一个问题,如果遇到一个函数调用,选择普通函数也合适,选择函数模板(泛化)也合适,选择函数模板特化版本也合适的时候,编译器考虑顺序是最优先选择普通函数,没有普通函数,才会考虑函数模板的特化版本,如果没有特化版本或者特化版本都不合适,才会考虑函数模板的泛化版本。如果恰好碰到两个模板特化版本都合适的,那编译器会选择那种最最合适的特化版本,例如,如果传递一个字符串给函数模板,函数模板特化版本中有数组类型模板参数和指针类型模板参数都可以接字符串类型,但在系统看来,很可能数组版本类型模板参数比指针类型模板参数更合适,所以系统会选择那个数组类型的模板参数的特化版本。

函数模板不能偏特化,编译会出现错误。

七、可变参模板与模板模板参数

C++11引入了可变参模板,允许模板定义中含有0到多个模板参数。

1、可变参函数模板

先来看一段代码:

template<typename... T>
void myfunct1(T... args) {  // T:包类型,args:包形参
    cout << sizeof...(args) << endl;  // sizeof...属于固定语法,用在可变参模板内部,用来表示收到的模板参数个数,只能针对这种...的可变参
    cout << sizeof...(T) << endl;  // 本行和上行效果一样
}

在 main 函数中加入如下代码:

myfunct1();  // 0
myfunct1(10, 20);  // 2
myfunct1(10, 25.8, "abc", 68);  // 4

T 称为“可变参类型”,里面包含0到多个不同的类型。args 称为“可变形参”,是一包(或者一堆)参数。

template<typename T, typename...U>
void myfunct2(const T& firstarg, const U& ...otherargs) {
    cout << sizeof...(firstarg) << endl;  // 编译会出错,sizeof...只能用在一包类型或者一包形参上
    cout << sizeof...(otherargs) << endl;
}

在 main 函数中加入如下代码:

// myfunct2();  // 语法错,必须要有一个firstarg
myfunct2(10);  // firstarg对应第一个参数,因为没有其他参数,所以sizeof...(otherargs) = 0
myfunct2(10, "abc", 12.7);  // firstarg对应第一个参数,剩余两个参数,所以sizeof...(otherargs) = 2

那如何展开参数包呢?一般用递归的方式,要在代码中编写一个参数包展开函数和一个同名的递归终止函数,通过这两个函数将参数包展开。一般把可变参函数模板写成myfunct2这种形式,带一个单独的参数,后面跟一个“一包参数”,最适合参数包的展开。先写一个参数包展开函数:

template<typename T, typename...U>
void myfunct2(const T& firstarg, const U& ...otherargs) {
    cout << "收到的参数值为:" << firstarg << endl;
    myfunct2(otherargs...);  // 递归调用,注意塞进来的是一包形参,这里...不能省略
}

再写一个同名的递归终止函数,放在刚刚myfunct2的上面。一般带0个参数的同名函数,就是递归终止函数:

// 因为参数是被一个一个剥离,到最后参数个数就为0个,此时就会调用到这个版本的myfunct2()
void myfunct2() {  // 这是一个普通函数,而不是函数模板
    cout << "参数包展开时执行了递归终止函数 myfunct2()" << endl;
}

在 main 函数中,写下这行代码:

myfunct2(10, "abc", 12.7);

运行结果如下:

收到的参数值为:10
收到的参数值为:abc
收到的参数值为:12.7
参数包展开时执行了递归终止函数 myfunct2()

第一次调用myfunct2firstarg拿到10,剩余2个参数被otherargs拿到了,输出10。第二次调用myfunct2otherargs里面的2个参数一个被拆分给了firstarg,剩余1个被otherargs拿到了,输出abc。以此类推,最终当这一包参数为空的时候,此时firstargotherargs都为空,就会调用void myfunct2()。这个调用过程其实就是:

myfunct2(10,"abc", 12.7);
myfunct2("abc", 12.7);
myfunct2(12.7);
myfunct2();

2、可变参类模板

同样的,可变参类模板也允许模板定义中含有0到多个模板参数,但其参数包的展开方式和可变参函数模板不一样,第一种是通过递归继承方式展开参数包,来看一个可变参模板的范例:

// 主模板定义(泛化版本的类模板)
template<typename ...Args>
class myclasst {
   public:
    myclasst() {
        printf("myclasst::myclasst()泛化版本执行了, this = %p\n", this);
    }
};
// 主模板声明可以这么写(这样写不是定义,是声明,所以不能用来创建对象)
// template<typename ...Args> class myclasst;

template<typename First, typename... Others>
class myclasst<First, Others...> : private myclasst<Others...> {  // 偏特化
   public:
    myclasst() : m_i(0) {
        printf("myclasst::myclasst()偏特化版本执行了,this = %p, sizeof...(Others) = %d\n", this, sizeof...(Others));
    }

    First m_i;
};

上面代码中,参数也是分开成一个和一包。在 main 主函数中,加入如下代码:

myclasst<int, float, double> myc;

执行结果如下:

myclasst::myclasst()泛化版本执行了,this = 012FFE44
myclasst::myclasst()偏特化版本执行了,this = 012FFE44,sizeof...(Others) = 0
myclasst;:myclasst()偏特化版本执行了,this = 012FFE44,sizeof...(Others) = 1
myclasst::myclasst()偏特化版本执行了,this = 012FFE44,sizeof...(Others) = 2

这里执行了4个构造函数,系统实例化出来了4个类。先来说其中的后三个类,执行代码行myclasst<int, float, double> myc;时,系统会去实例化三个类型模板参数的类模板。class myclasst<First, Others...> : private myclasst<Others...>它继承的是两个类型模板参数的类,而两个类型模板参数的类模板继承的是一个类型模板参数的类模板,因此系统首先会去实例化带一个类型模板参数的类模板,然后实例化带两个类型模板参数的类模板,最后实例化带三个类型模板参数的类模板,继承关系如图所示:

示例图片

这种继承方法是:把一包拆成一个和一包,那剩余这一包,因为每次都分出去一个,就会变得越来越小。拆到第三次,所继承的这个父类(myclasst<Others...>)就会是继承一个模板参数为0个的这么一个很特殊的特化版本,而这个特化版本并不满足classmyclasst<First, Others...>这种格式(因为这种格式要求必须带至少一个模板参数)。编译器遇到带0个模板参数的类模板时,就停止了类模板的继承。同时,这种带0个模板参数的类模板是通过myclasst的泛化版本(也就是myclasst主模板的定义)实例化出来的。所以,最终得到的完整的可变参类模板递归继承方式层次图应该如图所示:

示例图片

结合图来看,完整的实例化顺序应该是:①实例化带0个类型参数的类(先执行的是主模板类的构造函数;②实例化带1个类型模板参数的类;③实例化带2个类型模板参数的类;④实例化带3个类型模板参数的类。前面说过,这种带0个模板参数的类模板的实例化是通过myclasst的泛化版本实例化出来的,那如果自己写一个模板参数为0的特化版本当然也是可以的:

template<> class myclasst<> {  // 一个特殊的特化版本
   public:
    myclasst() {
        printf("myclasst<>::myclasst()特殊的特化版本执行了,this = %p\n", this);
    }
};

执行结果发生了变化(第一行):

myclasst<>::myclasst()特殊的特化版本执行了,this = 001AF940
myclasst::myclasst()偏特化版本执行了,this = 001AF940,sizeof...(Others) = 0
myclasst::myclasst()偏特化版本执行了,this = 001AF940,sizeof...(Others) = 1
myclasst::myclasst()偏特化版本执行了,this = 001AF940,sizeof...(Others) = 2

myclasst<>做了老祖宗类。现在在上面的偏特化代码中,增加一个有参数的构造函数,让它变得更实用一些。这里要在初始化列表中给成员变量赋值,还要调用父类的有参构造函数,完整的偏特化写法:

template<typename First, typename... Others>
class myclasst<First, Others...> : private myclasst<Others...> {  // 偏特化
   public:
    myclasst() : m_i(0) {
        printf("myclasst::myclasst()偏特化版本执行了,this = %p,sizeof...(Others) = %d\n", this, sizeof...(Others));
    }
    // 注意第二个参数,这一包东西的写法
    myclasst(First parf, Others... paro) : m_i(parf), myclasst<Others...>(paro...) {
        cout << "--- begin ---" << endl;
        printf("myclasst::myclasst(parf, ...paro)执行了,this = %p\n", this);
        cout << "m_i = " << m_i << endl;
        cout << "--- end ---" << endl;
    }
    First m_i;

在 main 函数中加入以下代码:

myclasst<int, float, double> myc(12, 13.5, 23);

执行结果是:

myclasst<>::myclasst()特殊的特化版本执行了,this = 012FFE40
--- begin ---
myclasst::myclasst(parf, ...paro)执行了,this = 012FFE40
m_i = 23
--- end ---
--- begin ---
myclasst::myclasst(parf, ...paro)执行了,this = 012FFE40
m_i = 13.5
--- end ---
--- begin ---
myclasst::myclasst(parf, ...paro)执行了,this = 012FFE40
m_i = 12
--- end ---

上述就是通过递归继承方式展开参数包,接下来看第二种方式,通过递归组合方式展开参数包。组合其实就是一种包含关系:

class B {
};

class A {
   public:
    B b;
};

A 中包含 B 对象,A 与 B 之间就是一种组合关系。将上面可变参数类模板myclasst的偏特化代码调整一下,达成“通过递归组合方式展开参数包”的目的:

template<typename First, typename... Others>
class myclasst<First, Others...>  // : private myclasst<Others...>  // 偏特化
{
   public:
    myclasst() : m_i(0) {
        printf("myclasst::myclasst()偏特化版本执行了,this = %p,sizeof...(Others) = %d\n", this, sizeof...(Others));
    }
    // 注意第二个参数,这一包东西的写法
    myclasst(First parf, Others... paro) : m_i(parf), m_o(paro...)//, myclasst<Others...>(paro...)
    {
        cout << "--- begin ---" << endl;
        printf("myclasst::myclasst(parf, ...paro)执行了,this = %p\n", this);
        cout << "m_i = " << m_i << endl;
        cout << "--- end ---" << end1;
    }
    First m_i;
    myclasst<Others> m_o;
};    

上面的代码和继承相关的那部分代码已经注释掉,而后增加了一个m_o成员变量,main 主函数中代码不变,依旧是:

myclasst<int, float, double> myc(12, 13.5, 23);

执行结果是:

myclasst<>::myclasst()特殊的特化版本执行了,this = 002AFD04
--- begin ---
myclasst::myclasst(parf, ...paro)执行了,this = 002AFCFC
m_i = 23
--- end ---
--- begin ---
myclasst::myclasst(parf,...paro)执行了,this = 002AFCF4
m_i = 13.5
--- end ---
--- begin ---
myclasst::myclasst(parf,...paro)执行了,this = 002AFCEC
m_i = 12
--- end ---

跟改造之前(通过递归继承方式展开参数包)代码的结果进行比较,能发现一些问题:①通过递归继承方式展开参数包执行的结果中,打印出的this值都相等,这说明实例化出来的这个对象由很多子部分构成;②通过递归组合方式展开参数包执行的结果中,打印出的this值都不相等,这说明产生了几个不同的对象。可变参类模板递归组合方式展开参数包所形成的层次关系如图所示:

示例图片

最后,来看第三种展开参数包的方式,通过 tuple 和递归调用展开参数包。tuple 也叫元组,里面能装各种不同类型的元素(数据),将下面的代码放入 main 函数中:

tuple<float, int, int> mytuple(12.5f, 100, 52);  // 一个tuple(元组):一堆各种类型数据的组合,元组可以打印,用标准库中的get(函数模板)
cout << get<0>(mytuple) << endl;  // 12.5
cout << get<1>(mytuple) << endl;  // 100
cout << get<2>(mytuple) << endl;  // 52

在 main 函数上面(外面)写一个可变参函数模板:

template<typename...T>
void myfunctuple(const tuple<T...>& t) {  // 可变参函数模板
    // ......
}

在 main 函数中继续加入如下代码:

myfunctuple(mytuple);  // 成功调用myfunctuple

有了 tuple 使用的简单认知,就可以看一看如何通过 tuple 和递归调用展开参数包。具体的实现思路就是有一个计数器(变量)从0开始计数,每处理一个参数,这个计数加1,一直到把所有参数处理完,最后提供一个模板偏特化,作为递归调用结束。

// 类模板的泛化版本
template<int mycount, int mymaxcount, typename ...T>  // mycount 用于统计,从0开始,mymaxcount表示参数数量,可以用sizeof...取得
class myclasst2 {
   public:
    // 下面的静态函数借助tuple(类型)、借助get(函数)就能够把每个参数提取出来
    static void mysfunc(const tuple<T...>&t) {  // 静态函数。注意,参数是tuple
        cout << "value = " << get<mycount>(t) << endl;  // 可以把每个参数取出来并输出
        myclasst2<mycount + 1, mymaxcount, T...>::mysfunc(t);  // 计数每次+1,这里是递归调用,调用自己
    }
};

然后必须要有一个特化版本,用于结束递归调用:

// 偏特化版本,用于结束递归调用
template<int mymaxcount, typename ...T>
class myclasst2<mymaxcount, mymaxcount, T...> {  // 注意“<>”中前两个都是mymaxcount
   public:
    static void mysfunc(const tuple<T...> &t) {
        // 这里其实不用干啥,因为计数为0、1、2是用泛化版本中的mysfunc处理,到这里时是3,不用做什么处理了
    }
};

继续完善上面的可变参函数模板myfunctuple的代码,在其中将使用刚刚定义的myclasst2类模板:

template<typename...T >
void myfunctuple(const tuple<T...>& t) {  // 可变参函数模板
    myclasst2<0, sizeof...(T), T...>::mysfunc(t);  // 注意第一个参数是0,表示计数从0开始
}

main 函数中的代码保持不变,还是:

tuple<float, int, int> mytuple(12.5f, 100, 52);
myfunctuple(mytuple);

执行结果是:

value = 12.5
value = 100
value = 52

3、模板模板参数

先看一个最简单的类模板范例:

template<typename T, typename U>
class myclass {
   public:
    T m_i;
};

T 和 U 都称为模板参数,在这里代表类型,因为它们前面有typename,所以又叫类型模板参数。这里引入一个新概念:模板模板参数,表示这个模板参数本身又是一个模板。如果把 U 这个类型模板参数改成模板模板参数,看一看代码:

template<
    typename T,  // 类型模板参数
    template<class> class Container  // 这就是一个模板模板参数,写法比较固定.这里的名字叫Container,实际上叫U也可以,因为模板模板参数一般是做容器用,所以这里取名Container
>
class myclass {
   public:
    Container<T> myc;
   public:
    T m_i;
};

class myclass里面增加使用这个Container的代码行,Container 作为一个类模板来使用(因为它的后面带着<T>,所以它是一个类模板)。所以如果想把 Container 当成一个类模板来用,就必须把它变成一个模板模板参数。从整体来看,template<class> class Container是 myclass 这个类模板的模板参数,而 Container 本身也是一个模板,所以 Container 的完整名字就叫模板模板参数。下面的代码和刚刚所写的代码效果完全相同:

template<
    typename T,
    // template<class> class Container
    template<typename W> typename Container
>
class myclass {
    // ......
};

上面代码中的 W 没有什么用,可以省略。只有用这种模板模板参数的技术才能够在 myclass 中写出这种Container<T> myc;。可能有人认为,既然 Container 是一个类型,那就这样写:

template<
    typename T,
    // template<class> class Container
    // template<typename W> typename Container
    typename Container
> ......

如果像上面这样写代码,编译就会报错。提示:error C2059: 语法错误:“<”。然后继续,在myclass类模板中增加一个public修饰的构造函数:

public:
    myclass() {  // 构造函数
        for (int i = 0; i < 10; ++i) {
            myc.push_back(i);  // 这行码是否正确取决于实例化该类模板时所提供的模板参数类型
        }
    }

编译一下,没有问题。在 main 主函数中增加如下代码:

myclass<int, vector> myvecobj;  // 本意:这里int是容器中的元素类型,vector是容器类型

编译一下,发现报错:类模板”std::vector“的模板参数列表与模板参数”Container“的模板参数列表不匹配(C++17 新标准已不会再报错)。之所以报错,是因为其实容器 vector 还有个分配器作为第二个参数。第二个参数一般来说不需要提供,是有默认值的,但是在这里涉及模板模板参数传递,似乎这个默认值就失灵了,编译器可能推导不出这个分配器的类型,那就得靠程序员解决这个编译错误。前面讲过“using定义模板别名”,此时就需要用到这个技术,在 main 主函数的上面找个位置,给容器定义别名。代码如下:

template<typename T>
using MYVec = vector<T, allocator<T>>;  // 这个写法其实也很固定
template<typename T>
using MYList = list<T, allocator<T>>;  // list容器,理解成和vector功能类似即可

在 main 主函数中加入如下代码:

myclass<int, MYVec> myvecobj;
myclass<int, MYList> mylistobj;

编译通过,问题解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值