C++之模板

模板与泛型编程

一、模板相关概念

1.1 什么是模板

1.1.1 模板定义

模板,从现实中来看就是一个通用的格式,我们按照这样的格式去处理这一类事情。同样的,在编程中模板就是一个创建函数的蓝图或者说是公式。

1.1.2 模板与泛型编程的关系
  • 模板是泛型编程的基础
  • 将模板转换为特定的类或函数,这种转换发生在编译时。
  • 编写一个泛型程序时,是独立于任何特定类型来编写代码的。当使用一个泛型程序时,用户提供类型或值,程序实例可在其上运行。

1.2 面向对象编程(OOP)与泛型编程对比

  • 相同点:两者都能处理在编写程序时不知道类型的情况。
    • 如oop中动态多态的例子,利用基类作为左操作数来实例化一个派生类对象,然后利用基类变量来使用派生类对象的成员函数,从程序上来看,并不知道基类要初始化哪个派生类对象,也不知道调用的是哪个派生类函数所实现的内容。即动态多态的特点:一个接口,多个功能
    • 泛型编程也是在使用时传入给定的类型,如vector<T>,若我们想使用vector容器来存储int类型,那么我们就使用vector<int>来定义变量
  • 不同点:程序生成过程上的要求有所不同
    • 在OOP能处理类型在程序运行之前都未知的情况,如动态多态,在运行时才去执行指定对象的函数。
    • 而在泛型编程中,在编译时就能获知类型了。

1.3 为什么要定义模板?

  1. 简化程序,少写代码,维持结构的清晰,提高了程序的效率。
  2. 解决强类型语言的严格性和灵活性之间的冲突

二、定义模板

模板有适用于两种类型:函数模板和类模板

2.1 函数模板

一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。

2.1.1 引入

之所以定义函数模板(function template),是为了解决重复定义完全一样的函数体,但由于变量类型导致需要重载定义的问题。

如下面的例子:

我们想要去比较两个int类型的大小,同时我们还想要比较string类型,那么就要写两个compare函数,尽管函数体一样,但还是要定义两个函数去完成,这样就显得非常的繁琐。

#include <iostream>
#include <string>

using std::endl;
using std::cout;
using std::string;

bool compare(const int &a, const int &b) {
    return a < b;
}

bool compare(const string &s1, const string &s2) {
    return s1 < s2;
}

所以,这时就引入了模板。

#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;


template<class T>
bool compare(const T &t1, const T &t2) {
    return t1 < t2;
}


int main() {
    int a = 10, b = 2;
    string s1 = "hello", s2 = "helww";
    
    cout << compare(a, b) << endl;
    cout << compare(s1, s2) << endl;
    return 0;
}

我们发现,即使没有写出具体类型的函数实现,也能够成功使用函数。即使用模板时,是在编译时就确定了函数参数类型,在之后还会说明。

2.1.2 函数模板的格式

在要定义的函数或类之前加上template<class T, ...>或者template <typename T, ...>

模板关键字:template

<typename T,...>是模板参数列表(template parameter list) ,template T是模板参数(template parameter)

在模板定义种,模板参数列表不能为空

可以看出,模板参数表示在类或函数定义种用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板实参(template argument),将其绑定到模板参数上。

隐式:compare(1, 2)这样编译器会自动推断类型

显式:compare<int>(1,2),给函数指定确定的int类型

2.1.3 实例化函数模板

当我们调用一个函数模板时,编译器会用函数实参来为我们推断模板实参。即我们在调用函数时,编译器使用实参的类型来确定绑定到模板参数T的类型。

编译器用推断出的模板参数来为我们实例化一个特定版本的函数当编译器实例化一个模板时,它会生成一个实际参数模板函数。

问题引入:

编译器也不是万能的,当我们传入参数时,编译器会为我们推断,这显然不是一件简单的工作;当我们写入两个不同类型的参数时,此时的编译器会很为难,因为它并不知道要使用哪个参数类型来为我们实例化函数。

compare(1, 2.3)第一个参数是整型,第二个参数是浮点型,那么编译器也不知道将实例化一个哪个类型的函数;或者从另一个方面来说,compare模板参数都是未知类型T,在编译器编译的时候,T到底是第一个参数的类型呢,还是第二个参数的类型呢,显然有二义性,所以这是一个问题,也是实例化方式的问题。

实例化函数模板时的方式

  • 隐式实例化:没有明确说明类型,靠编译器推导。上述问题引入的部分也说明了隐式实例化的问题所在。
    在这里插入图片描述
    这里提示了没有匹配函数调用’compare’,这里的原因是由于模板函数类型的确定是在编译期,当传入的参数,编译器分辨不出来是哪个的时候,相当于没有调用函数,也就导致了没有实例化出这样一个类型的模板函数。
    在这里插入图片描述

  • 显式实例化:编译器无须推导参数类型

在这里插入图片描述

这里指定了模板函数的类型int,编译器进行的处理是将类型做了一个转换。

2.1.4 函数模板和普通函数见的关系与区别
关系
  1. 函数模板与普通函数是可以进行重载的
  2. 普通函数优先于函数模板执行
  3. 函数模板与函数模板之间也是可以进行重载的

接下来我们将使用代码的方式进行验证

#include <iostream>
#include <string>

using std::endl;
using std::cout;
using std::string;

bool compare(const int &a, const int &b) {
    cout << "normal function int" << endl;
    return a < b;
}

bool compare(const string &s1, const string &s2) {
    cout << "normal function string" << endl;
    return s1 < s2;
}


template<class T>
bool compare(const T &t1, const T &t2) {
    return t1 < t2;
}
//重载
template<class T>
bool compare(const T &t1, const T &t2, const T &t3) {
    return t1 > t2 ? (t1 > t3 ? true : false) : false;
}

void test1() {
    int a = 10, b = 2, c = 5;
    string s1 = "hello", s2 = "helww";
    cout << compare(a, b) << endl;
    cout << compare<int>(1, 2.3) << endl;
    cout << compare(s1, s2) << endl;
    cout << compare(a, b, c);
}

int main() {
    test1();

    return 0;
}

normal function int
0
1
normal function string
1

从结果来看,上面的关系是正确的,并且我们还发现,显式实例化之后的函数模板,调用的是函数模板而不是普通函数

区别
  1. 普通函数调用时可以发送自动类型转换(隐式)
  2. 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  3. 如果利用显式指定类型的方式,可以发生隐式类型转换

这里的2和3在实例化部分已经验证过

总结

建议使用显式指定类型的方式去调用函数模板,因为可以自己确定通用类型T。

2.1.5 函数模板的参数类型
  1. 类型参数,如class T
  2. 非类型参数 常量表达式,整型:bool/char/short/int/long等,注意:float/double这些就不是整型
//这里我们可知又多了一种常量的书写形式
template <typename T = int, short kMin = 10>
T multiply(T x, T y) {
	return x * y * kMin;
}

2.2 模板编译的相关问题(重要)

2.2.1 模板编译
  1. 当编译器遇到了一个模板定义时,它并不生成代码只有当我们实例化出模板的一个特定版本时,编译器才会生成代码

  2. 实例化,即我们在使用模板时,即调用模板,像上面的compare(a,b)时,编译器才会生成一个参数列表为int类型的函数模板实例化出的函数。

这两点很重要,在使用模板的时候,很容易在这个地方犯错。

  1. 通常,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,模板的头文件通常即包括声明也包括定义。函数模板和类模板成员函数的定义通常放在头文件中

接下来我们来写一段代码来验证:

#include <iostream>
using std::endl;
using std::cout;
class Computer1 {
public:
    void showComputer1() {
        cout << "I am Computer1" << endl;
    } 
};

class Computer2 {
public:
    void showComputer2() {
        cout << "I am Computer2" << endl;
    } 
};

template <class T>
class MyClass {
public:
    T obj;
	//类模板中的成员函数,并不是一开始就创建的,而是在模板调用时再生成
    void func1() {
        obj.showComputer1();
    }
    void func2() {
        obj.showComputer2();
    }

};


int main()
{
    MyClass<Computer1> mc;
    mc.func1();	
    mc.func2();	//error
    return 0;
}

此时我们直接编译,并没有报错,验证了只有在调用时才会创建类模板中的函数。

2.2.2 大多数编译错误在实例化期间报告

模板的特性:模板直到实例化时才会生成代码。这也影响了我们何时才会获知模板内代码的编译错误。通常,编译器会在三个阶段报告错误。

  1. 第一个阶段是编译模板本身。在定义模板时由于一些语法规则导致的错误。
  2. 第二个阶段编译器遇到模板的使用时。对于函数模板的调用,编译器通常会检查实参数目是否正确,它还能检查参数类型是否匹配;对于类模板,编译器可以检查用户是否提供了正确数目的模板实参。
  3. 第三个阶段是模板实例化时,依赖于编译器如何管理实例化,这类错误可能在链接时才报告。

下面我们来一一列举这些报错信息:

  1. 在这里插入图片描述

cout << compare(1, 2.3) << endl; //这个在上面的实例化模板上讲过
  1. 为了验证实例化的错误,我们自定义一个Point类,不重载<运算符,然后利用compare模板函数对两个Point类对象进行比较

    #include <iostream>
    #include <math.h>
    using std::endl;
    using std::cout;
    
    class Point {
        friend bool operator<(const Point &lhs, const Point &rhs);
    public:
        Point(int ix, int iy) 
            :_ix(ix), _iy(iy) { 
                cout << "Point(int, int)" << endl;
            }
        ~Point() {
            cout << "~Point()" << endl;
        }
        
        void print() const;
        //获取当前point到原点的距离
        double getDistance() const;
    
    private:
        int _ix;
        int _iy;
    
    };
    
    void Point::print() const {
        cout << "(" << _ix << "," << _iy << ")" << endl;
    }
    
    double Point::getDistance() const {
        return sqrt(_ix * _ix + _iy * _iy); 
    }
    
    #if 0		//注意这里是关闭的,#if 0 #endif相当于一个注释的作用
    bool operator <(const Point &lhs, const Point &rhs) {
        return lhs.getDistance() < lhs.getDistance();
    }
    #endif
    
    template<class T>
    bool compare(const T &t1, const T &t2) {
        return t1 < t2;
    }
    
    void test2() {
        Point p1(1,2);
        Point p2(3,4);
        cout << compare(p1, p2) << endl;
    }
    
    int main() {
        test2();	
    }
    
    g++ functionTemplate.cc
    /tmp/ccn6461r.o: In function `bool compare<Point>(Point const&, Point const&)':
    functionTemplate.cc:(.text._Z7compareI5PointEbRKT_S3_[_Z7compareI5PointEbRKT_S3_]+0x1f): undefined reference to `operator<(Point const&, Point const&)'
    collect2: error: ld returned 1 exit status
    

    我们发现,当生成可执行程序时,出现了链接错误,原因就是没有定义Point类的<运算符。

总结:通过这些错误,我们可以得出定义的模板函数中函数体的内容,只是一种模板化的操作;而这些操作能不能正常使用,是调用者的责任。

2.3 类模板

类模板(class template)是用来生成类的模板。

与函数模板不同的是,编译器不能为类模板推断模板的参数类型

这是很显然的事实,比如,在动物中,说一个动物能飞翔,但现实中能飞翔的动物有很多,不指明哪一种动物,别人肯定无法正确地做出猜测。

所以为了使用类模板,我们必须在模板名后的尖括号中提供额外信息,用来代替模板参数的模板实参列表,如vector<int>

2.3.1 类模板的定义方式

类模板以关键字template开始,后跟模板参数列表。在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值,这里我们举一个例子

#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;
//在本例中,我们使用了两个模板参数
template <class T1, class T2>
class TemplateClass {
public:
    TemplateClass(const T1 &t1, const T2 &t2)
        :_t1(t1),
        _t2(t2) { 
            /* cout << "TemplateClass(const T1 &, const T2 &)" << endl; */
        }
    ~TemplateClass() {
        /* cout << "~TemplateClass()" << endl; */
    }

    void print() const {
        cout << "(" << _t1 << ", " << _t2 << ")" << endl;
    }
private:
    T1 _t1;
    T2 _t2;
};

void test1() {
    TemplateClass<int,int> tc(1,2);    
    tc.print();

    TemplateClass<string, string> tc2("hello", "world");
    tc2.print();
    TemplateClass<string, int> tc3("xiaoming", 13);
    tc3.print();
}

int main() {
    test1();
}

/*
(1, 2)
(hello, world)
(xiaoming, 13)
*/
2.3.2 实例化类模板

当我们使用类模板实例化类时,必须要提供显示的模板实参(explicit template argument)列表,它们被绑定到模板参数。编译器使用这些模板实例来实例化出特定的类。

对于我们指定的每一种元素类型,编译器都生成一个不同的类。(2.3.1的代码test1())

这里我们再来验证两个相同模板参数类型,内容相同的类对象是否是相同的:

#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;

//前置声明
template <class T1, class T2> class TemplateClass;
template <class T1, class T2>
bool operator == (const TemplateClass<T1, T2> &t1, const TemplateClass<T1, T2> &t2);


template <class T1, class T2>
class TemplateClass {
    friend bool operator == <T1, T2>(const TemplateClass<T1, T2> &t1, const TemplateClass<T1, T2> &t2);
    
public:
    TemplateClass(const T1 &t1, const T2 &t2)
        :_t1(t1),
        _t2(t2) { 
            /* cout << "TemplateClass(const T1 &, const T2 &)" << endl; */
        }
    ~TemplateClass() {
        /* cout << "~TemplateClass()" << endl; */
    }

    void print() const {
        cout << "(" << _t1 << ", " << _t2 << ")" << endl;
    }
private:
    T1 _t1;
    T2 _t2;
};
/* template <> */
template<class T1, class T2>
bool operator==(const TemplateClass<T1, T2> &t1, const TemplateClass<T1, T2> &t2) {
    return t1._t1 == t2._t1 && t1._t2 == t2._t2;
}

void test2() {
    TemplateClass<string, string> tc1(string("hello"), string("world"));
    TemplateClass<string, string> tc2(string("hello"), string("world"));
    
    if(tc1 == tc2) {
        cout << "tc1 == tc2" << endl;
    }
    else {
        cout << "tc1 != tc2" << endl;
    }
}

int main() {
    test2();
}
tc1 == tc2

这里我们可以看到结果是tc1和tc2是相等的。所以可以说每个实例化的类模板,编译器都会给其生成一个独立的类。相同的模板参数类型的对象是属于同一个类中的。

注:这里设计到了友元与模板类之间的关系,看不懂可以后跳,这里只是为了验证类的实例化对象,看例子即可。可以在2.4.2中查看关于友元与类模板的部分

2.3.3 类模板的成员函数及其实例化

类模板的成员函数也和普通的类的成员函数一样,可以定义在类的内部,同样也可以在类内部声明,定义在类的外部,但是相对要多注意一些细节。

同样,定义在类模板内的成员函数被隐式声明为内联函数,这个跟普通类的特点一样。

下面我们提出几点类模板中成员函数的注意事项:

  1. 类模板的成员函数本身也是一个普通的函数,但是,类模板的每个实例都有着其自己版本的成员函数。因此,类模板的成员函数具有和模板类相同的模板参数
  2. 在定义类模板之外的成员函数就必须以关键字template开始,后接模板参数列表。这里和函数模板一样。
  3. 当我们在类外定义一个成员时,必须说明成员属于哪个类。(注意:成员包括成员函数和成员变量。)而且,从一个模板生成的类的名字中必须包含其模板实参。
  4. 当我们定义一个成员函数时,模板实参与模板形参相同。
实例化
  1. 默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。

  2. 如果一个成员函数没有被使用,则它不会被实例化。

  3. 成员函数只有被在使用的时候才进行实例化,这些特性使得即使某种类型不能完全符合模板操作的要求,但我们仍然能用该类型实例化类。

  4. 不止是成员函数,默认情况下,对于一个实例化了的类模板,其成员只有在其使用时才被实例化

这里我们通过代码的方式去验证说法:

#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;

template <class T1, class T2>
class TemplateClass {
    friend bool operator == <T1, T2>(const TemplateClass<T1, T2> &t1, const TemplateClass<T1, T2> &t2);
    
public:
    TemplateClass(const T1 &t1, const T2 &t2)
        :_t1(t1),
        _t2(t2) { 
            /* cout << "TemplateClass(const T1 &, const T2 &)" << endl; */
        }
    ~TemplateClass();

    void print() const {	//类内定义
        cout << "(" << _t1 << ", " << _t2 << ")" << endl;
    }    
    void display() const;

    T1 getT1() const;

private:
    T1 _t1;
    T2 _t2;
};
	
template<class T1, class T2>	//类外定义,析构函数
TemplateClass<T1, T2>::~TemplateClass() {  }

template <class T1, class T2>	//类外定义,成员函数
void TemplateClass<T1, T2>::display() const {
    cout << "I am display()" << endl;
}

template<class T1, class T2>	//类外定义,带返回值的成员函数
T1 TemplateClass<T1 ,T2>::getT1() const { return _t1; }


void test3() {
    TemplateClass<string, int> tc1("hello", 12);
    tc1.display();
    string s1 = tc1.getT1();
    cout << "s1 = " << s1 << endl;
}

int main() {
	test3();
}
/*
I am display()
s1 = hello
*/
在类代码内简化模板类名的使用

当我们在使用一个类模板类型时,必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以使用模板名,而不提供实参。

template <class T1, class T2>
class TemplateClass {
    ...
    ...
    map<T1, T2> mtt;
};
在类模板外使用类模板名

当我们在类模板外定义其成员时,我们此时并不在类的作用域内,直到遇到类名才表示进入类的作用域,即TemplateClass::

template <class T1, class T2>
class TemplateClass {
	...
    void display() const;
	...
};

template <class T1, class T2>	//类外定义,成员函数
void TemplateClass<T1, T2>::display() const {
    cout << "I am display()" << endl;
}
2.3.4 成员函数模板

除了上述所说,类模板的成员函数具有和模板类相同的模板参数之外,类的成员函数也可以设置自己的模板

在类外定义时,需要注意写法,将模板类的template部分写到最前面,成员函数的template部分紧跟其后

#include <iostream>
#include <string>
#include <map>
using std::endl;
using std::cout;
using std::string;
using std::map;


template <class T1, class T2>
class TemplateClass {
    friend bool operator == <T1, T2>(const TemplateClass<T1, T2> &t1, const TemplateClass<T1, T2> &t2);
    
public:
    TemplateClass(const T1 &t1, const T2 &t2)
        :_t1(t1),
        _t2(t2) { 
            /* cout << "TemplateClass(const T1 &, const T2 &)" << endl; */
        }
    ~TemplateClass();


    template <typename F>
    F func1(T1 t1) {
        return (F)t1;
    }

    template <typename F>
    F func2(T2 t2);
    
    template <typename F>
    F func3();
private:
    T1 _t1;
    T2 _t2; 
};

template<class T1, class T2>
template <typename F>
F TemplateClass<T1, T2>::func2(T2 t2) {
    return F(t2);
}

template<class T1, class T2>
template <typename F>
F TemplateClass<T1, T2>::func3() {
    return F(_t2);
}



void test4() {
    TemplateClass<double, double> tc1(3.1415, 4.412);
    int ret1 = tc1.func1<int>(8.1415);
    char ret2 = tc1.func2<char>(97);
    int ret3 = tc1.func3<int>();
    cout << "ret1 = " << ret1 << endl;
    cout << "ret2 = " << ret2 << endl;
    cout << "ret3 = " << ret3 << endl;

}

int main() {
    test4();
}

/*
ret1 = 8
ret2 = a
ret3 = 4
*/

2.4 类模板和友元

当一个类包含友元声明时,类与友元各自是否是模板是相互无关的。

如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。

如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。(说白了就是类赋予的模板类型可用可不用)。

2.4.1 全局函数类内实现

直接在类内声明友元即可

#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;

template<class T1, class T2>
class Computer {
    //全局函数 内类实现
    friend void printComputer(Computer<T1, T2> c) {
        cout << c._name << ", " << c._price << endl; 
    }
public:    
    Computer(T1 name, T2 price)
        :_name(name),
        _price(price) { 
        }
    ~Computer() {
    }
private:
    T1 _name;
    T2 _price;
};


//1.全局函数在类内实现
void test1() {
    Computer<string, double> c("lenovo", 12200);
    printComputer(c);
}

int main() {
    test1();

    return 0;
}
/*
lenovo, 12200
*/
2.4.2 全局函数类外实现

需要提前让编译器直到全局函数的存在

类外实现时常遇见的问题

#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;

template<class T1, class T2>
class Computer {    
    //全局函数 类外实现
    friend void printComputer2(Computer<T1, T2> c);

public:    
    Computer(T1 name, T2 price)
        :_name(name),
        _price(price) { 
        }
    ~Computer() {
    }
private:
    T1 _name;
    T2 _price;
};

//类外实现
template<class T1, class T2>
void printComputer2(Computer<T1, T2> c) {
    cout << "类外实现: " << c._name << ", " << c._price << endl; 
}

//2.全局函数在类外实现
void test2() {
    Computer<string, double> c("lenovo", 12200);
    printComputer2(c);
}

int main() {
    test2();

    return 0;
}

看似没有问题的一个全局友元类的类外实现,并且编译器也没有给我们报错。但是在链接阶段就会出现问题。

接下来我们来梳理一下思路

  1. friend void printComputer2(Computer<T1, T2> c);首先这个函数属于普通的函数,并不是类的成员函数

  2. 而下方的类外实现写的却是一个函数模板的函数实现

  3. 一个普通函数的声明和一个函数模板的实现不是一套东西,那么要告诉编译器这是一个函数模板的声明,所以在声明部分要多加一个参数列表,只需要多加一个空模板参数列表就可以了friend void printComputer2 <>(Computer<T1, T2> c);

  4. 并且,如果全局函数是类外实现的话,需要让编译器提前知道这个函数的存在,我们怎么做呢?

    1. 要让编译器知道这个函数的存在,那么我们就要把这个函数模板的声明或定义写到这个类的前面,告诉编译器有这个函数模板的存在,并且其参数列表中还有一个Computer,因此我们还需声明class Computer

    现在让我们看一看代码是怎么写的

    #include <iostream>
    #include <string>
    using std::endl;
    using std::cout;
    using std::string;
    //前置声明
    template<class T1, class T2>
    class Computer;
    
    template<class T1, class T2>
    void printComputer2(Computer<T1, T2> c);
    
    
    template<class T1, class T2>
    class Computer {
        
        //全局函数 类外实现
        friend void printComputer2<>(Computer<T1, T2> c);
    public:    
        Computer(T1 name, T2 price)
            :_name(name),
            _price(price) { 
            }
        ~Computer() {
        }
    private:
        T1 _name;
        T2 _price;
    };
    
    //类外实现
    template<class T1, class T2>
    void printComputer2(Computer<T1, T2> c) {
        cout << "类外实现: " << c._name << ", " << c._price << endl; 
    }
    
    
    //2.全局函数在类外实现
    void test2() {
        Computer<string, double> c("lenovo", 12200);
        printComputer2(c);
    }
    
    int main() {
        test2();
    
        return 0;
    }
    
    

    从这里可以看出,全局函数的类外实现是真的很麻烦的,所以建议全局函数做类内实现,用法简单,而且编译器可以直接识别。

2.5 类模板对象做函数参数

类模板实例化出的对象,向函数传参的方式

一共有三种传入方式:

  1. 指定传入的类型 :直接显示对象的数据类型
  2. 参数模板化:将对象中的参数变为模板进行传递
  3. 整个类模板化:将这个对象类型,模板化进行传递
#include <iostream>
#include <string>
using namespace std;

template<class T1, class T2>
class Computer {
public:
    Computer(T1 brand, T2 price)
        :_brand(brand), _price(price) {

        }
    void showComputer() {
        cout << "品牌:" << _brand << ", 价格:" << _price << endl;
    }
private:
    T1 _brand;
    T2 _price;
};


//1.指定传入类型
//这时,不通过c去调用类中的showComputer函数
//而是将c传入一个函数中,让其作为一个实参,在这个函数体内部去
//调用showComputer成员函数
void printComputer1(Computer<string, int> &c ) {
    c.showComputer();
}
void test01() {
    Computer<string, int> c("lenovo", 8000);
    printComputer1(c);
}

//2.参数模板化
template<class T1, class T2>
void printComputer2(Computer<T1, T2> &c) {
    c.showComputer();
    cout << "T1的类型为:" << typeid(T1).name() << endl;
    cout << "T2的类型为:" << typeid(T2).name() << endl;
}
void test02() {
    Computer<string, int> c("macBook", 9000);
    printComputer2(c);
}

//3.整个类模板化
template<class T>
void printComputer3(T &t) {
    t.showComputer();
    cout << "T的类型为:" << typeid(T).name() << endl;
    
} 
void test03() {
    Computer<string, int> c("macBook pro", 12000);
    printComputer3(c);
}

int main()
{
    test01();
    cout << endl;
    test02();
    cout << endl;
    test03();
    cout << endl;
    return 0;
}
/*
品牌:lenovo, 价格:8000

品牌:macBook, 价格:9000
T1的类型为:NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
T2的类型为:i

品牌:macBook pro, 价格:12000
T的类型为:8ComputerINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEiE
/

一般情况下,第一种指定传入类型是最常用的,其他的太复杂

2.6 类模板与继承

当类模板碰到继承时,需要注意以下几点:

  1. 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  2. 如果不指定,编译器无法给子类分配内存
  3. 如果想灵活指定出父类中T的类型,子类也需变为类模板
#include <iostream>
using namespace std;

template<class T>
class Base {
    T t;
};
//错误,必须要知道父类中的T类型,才能继承给子类
/* class Son1 : public Base {}; */
class Son : public Base<int> {
};


//如果想灵活指定父类中T类型,子类也需要变类模板
template<class T1, class T2>
class Son2 : public Base<T2> {
    T1 t1;
};

void test1() {
    Son2<int, double> s2;
}

int main()
{
    return 0;
}

**总结:**如果父类是类模板,子类需要指定出父类中T的数据类型

2.7 模板的特化

当我们在特例化一个函数模板时,必须为原模版中的每个函数模板参数都提供实参。

为了指出我们正在实例化一个模板,应使用关键字template后跟一个空尖括号对(<>)

空尖括号指出我们将为原模版的所有模板参数提供实参

特化分为:全特化和偏特化

为什么要提供特化,因为编译器认为,对于特定的类型,如果能对某一功能更好的实现,那么就听你的。

//函数模板只能全特化
template<typename T>
T add(T x, T y) {
    return x + y;
}

template<>
const char *add(const char *pstr1, const char *pstr2) {
	size_t len = strlen(pstr1) + strlen(pstr2) + 1;
    char *ptmp = new char(len);
    strcpy(ptmp, pstr1);
    strcat(ptmp, pstr2);
    return ptmp;
}

三、可变参数模板

可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。

可变数目的参数被称为参数包(parameter packet)。

3.1 参数包类型

存在两种参数包:

  1. 模板参数包(template parameter packet),表示零个或多个模板参数;

    1. template<typename ... Args> class tuple;//tuple是元组的意思,其模板参数就是模板参数包
    2. Args标识符的左侧使用了省略号,在C++11中Args被称为”模板参数包“,表示可以接受任意多个参数作为模板参数,编译器将多个模板参数打包成”单个“的模板参数包。
  2. 函数参数包(function parameter packet),表示零个或多个函数参数。

    1. template<typename ...T> void f(T ... args);//args就是函数参数包
    2. args被称为函数参数包,表示函数可以接受多个任意类型的参数。
    3. 在C++11标准中,要求函数参数包必须唯一,且是函数的最后一个参数;模板参数包则没有。
    4. 当使用参数包时,省略号位于参数名称的右侧,表示立即展开该参数,这个过程也被称为解包
//Args是一个模板参数包,rest是一个函数参数包
//Args表示零个或多个模板类型参数
//rest表示零个或多个函数参数
template<typename T, typename ... Args>
void foo(const T &t, const Args& ... rest);

3.2 可变模板参数的优势

  1. 参数个数,对于模板来说,在模板推导的时候,就已经知道参数的个数了,也就是说在编译的时候就确定了,这样编译器就存在可能去优化代码
  2. 参数类型,推导的时候也已经确定了,模板函数就可以知道参数类型了

3.3 编写可变参数函数模板

可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。

为了终止递归,我们还需要定义一个非可变参数的同名函数。

这里我们以print函数举例

#include <iostream>
#include <string>
using namespace std;


//用来终止递归并打印最后一个元素的函数
//此函数必须在可变参数版本的print定义之前声明
template<typename T>
ostream &print(ostream &os, const T &t) {
    return os << t; //包中最后一个元素之后不打印分割符
}

//包中除了最后一个元素之外的其他元素都会调用这个版本的print
template<typename T, typename ... Args>
ostream &print(ostream &os, const T &t, const Args & ... rest) {
    os << t << ", ";    //打印第一个实参
    return print(os, rest ...); //递归调用,打印其他实参
}

int main() {
    print(cout, "hello", "world", 123, "www\n");
    return 0;
}
/*
hello, world, 123, www
*/

第一个版本的print负责终止递归并打印初始调用中的最后一个实参。

第二个版本的print是可变参数版本,它打印绑定到t的实参,并调用自身来打印函数参数包中的剩余值。

这段程序的关键部分是可变参数函数中对print的调用:return print(os, rest...);//递归调用,打印其他实参

我们的可变参数版本的print函数接受三个参数:一个ostream&,一个const T&和一个参数包。

其结果是rest中的第一个实参被绑定到t,剩余实参形参下一个print调用的参数包。

因此,在每个调用中,包中的第一个实参被移除,称为绑定到t的实参。

调用trest…
print(cout,i,s,42)is,42
print(cout,s,42)s42
print(cout,42)调用非可变参数版本的print

对于最后一个调用,两个函数提供同样好的匹配。但是,非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本。

:当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。

用作个人学习总结。

参考:C++_Primer

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值