【C++ Primer】模板与泛型编程

文章目录

    • 一、定义模板
      • 1、函数模板
      • (1)模板类型参数、非类型模板参数
      • (2)编写类型无关的代码、模板编译、大多数编译错误在实例化期间报告
      • (3)本节demo:
      • 2、类模板
      • (1)定义类模板、实例化类模板
      • (2)类模板的成员函数及实例化、在类代码内简化模板类名的使用、在类模板外使用类模板名
      • (3)模板类和友元、令自己的实例化类型作为友元(c++11)
      • (4)类模板别名(c++11)、类模板的static成员
      • 3、模板参数
      • (1)模板参数与作用域、模板声明、使用类的类型成员
      • (2)默认模板实参、模板默认实参与类模板
      • 4、成员模板
      • 5、控制实例化、效率与灵活性
      • 6、类模板和成员模板demo:
    • 二、模板实参推断
      • 1、类型转换与模板实参推断
      • 2、函数模板显式实参
      • 3、尾置返回类型与类型转换
      • 4、函数指针和实参推断
      • 5、模板实参推断和引用
      • (1)从左值引用函数参数推断类型
      • (2)从右值引用函数参数推断类型
      • 6、 理解std::move
      • 7、 转发
      • 8、 本节demo:
    • 三、重载与模板
    • 四、可变参数模板(c++11)
      • 1、编写可变参数函数模板
      • 2、包扩展
      • 3、本节demo:
    • 五、模板特例化

  面对对象编程OOP和泛型编程都能处理在编写程序时不知道类型的情况,而OOP能处理类型在程序运行之前都未知的情况,在泛型编程中,在编译时就能获知类型了。
  模版是C++泛型编程的基础,一个模版就是一个类或函数的蓝图或者说是公式:例如在使用vector这样的泛型类型,或者是find函数这样的泛型类型,我们可以将蓝图转换为特定的类或者是函数,这种转换发生在编译时。

一、定义模板

1、函数模板

  一个函数模版就是一个公式,可用来生成指定类型的函数版本。模版定义从template开始,后跟一个模版参数列表,用两个小尖括号包起来,参数列表不能为空。
  我们可以为函数定义一个模板,而不是为每一个类型定义一个函数。例如比较函数:

#include 

template <typename T>//T表示一个类型,而T的实际类型在编译时根据实际情况决定。
int compare(const T&v1, const T&v2)
{
    if(v1 < v2) 
        return -1;
    if(v1 > v2)
        return 1;
    return 0;
}

int main()
{
    int i1 = 10, i2 = 20;
    double d1 = 20.1, d2 = 20.01;
    unsigned u1 = 10, u2 = 10;
    std::cout << compare(i1, i2) << std::endl;//实例化。
    std::cout << compare(d1, d2) << std::endl;
    std::cout << compare(u1, u2) << std::endl;
}

  当我们调用template时,编译器会使用实参的类型来确定绑定到模版参数T上的类型,之后编译器利用推断出的模版参数来实例化一个特定版本的函数,这个过程被称之为实例化。

(1)模板类型参数、非类型模板参数

  编译器遇到一个模版的定义时,并不会产生代码,只有当我们实例化出模版的一个特定版本的时候,编译器才会产生代码。
  可以将模版的类型参数看作是类型说明符,类型参数可以用来指定函数的返回类型、函数的参数类型以及在函数体内用于变量的声明和类型转换,在类型参数之前必须加上typename或者class这两个关键字,这两个关键字含义相同,可以互换使用,可以定义多个类型参数。使用typename更好一点,因为我们还可以使用内置类型(非类)作为模版的类型参数。
  保证传递给模版的实参支持模版的所有操作,以及这些操作在模版中正确工作,非常重要。

#ifndef TEMPLATE_H
#define TEMPLATE_H

//使用多个类型参数。
template <typename T, typename U>
bool my_find(const T &v1, const T &v2, const U &val)
{
	for (auto i = v1; i != v2; ++i)
	{
		if(*i == val)
			return true;
	}
	return false;
}
#endif TEMPLATE_H

  非类型模版参数,可以使用一个非类型参数(一个值或者指针和引用)来指定非类型参数,编译器会使用字面常量的大小代替非类型参数,若是字符串还会加一。
  非类型模板参数的模板实参必须是常量表达式。

#ifndef BEGIN_END_H
#define BEGIN_END_H

template<typename T, unsigned N> T* begin(const T (&arr)[N])
{// error C2234: “arr”: 引用数组是非法的,(&arr即可)
	return arr;
}

template<typename T, unsigned N> T* end(const T(&arr)[N])
{
	return arr+N;
}

#endif BEGIN_END_H

  inline 和constexpr也可以修饰函数模板,放在返回值前面。

//inline放在模板列表之后,返回类型之前。
//打印任意大小和类型的数组的所有元素。
template <typename T>
inline void my_print(const T &arr)
{
	for (const auto i : arr)
		cout << i << " ";
	cout << endl;
}

//返回数组大小。
template<typename T, unsigned N> 
constexpr size_t arr_size(const T (&arr)[N])  
{  
    return N;  
}

(2)编写类型无关的代码、模板编译、大多数编译错误在实例化期间报告

【Note】:
1)模板中的函数参数是const的引用(保证函数可以用于不能拷贝的类型)。
2)函数体条件中判断仅仅使用<比较(大多数都定义了<比较的类型)。有的时候大部分都会定义!=运算符。所以模板程序应该尽量减少对实参类型的要求。

  模板编译和普通的编译不同,它是在使用实例化时编译器才生成代码,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,因此和非模板代码不同,模板的头文件通常既包括声明又包括定义。
【Note】:
1)模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。
2)模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

  保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。

(3)本节demo:

#include 
using namespace std;
//使用多个类型参数。
//定义标准库find函数。
template <typename T, typename U>
bool my_find(const T &v1, const T &v2, const U &val)
{
	for (auto i = v1; i != v2; ++i)
	{
		if(*i == val)
			return true;
	}
	return false;
}

//inline放在模板列表之后,返回类型之前。
//打印任意大小和类型的数组的所有元素。
template <typename T>
inline void my_print(const T &arr)
{
	for (const auto i : arr)
		cout << i << " ";
	cout << endl;
}

//定义标准库的begin和end。
template <typename T, unsigned N>
T * begin(const T (&arr)[N])
{
	return arr;
}

template <typename T, unsigned N>
T * end(const T (&arr)[N])
{
	return arr + N;
}

//返回数组大小。
template<typename T, unsigned N> 
constexpr size_t arr_size(const T (&arr)[N])  
{  
    return N;  
}

int main(int argc, char const *argv[])
{
	vector<int> vec{1, 2, 3, 4};
	list<string> lst{"ab", "cd", "ef"};
	cout << my_find(begin(vec), end(vec), 2) << endl;
	cout << my_find(begin(lst), end(lst), "ab") << endl;
    int arr[3] = {2,2,2};
    my_print(arr);
    for (auto i = begin(arr); i != end(arr); ++i)
    	cout << *i << " ";
    cout << endl << arr_size(arr) << endl ;
	system("pause");
	return 0;
}

【C++ Primer】模板与泛型编程_第1张图片

2、类模板

  类模板是生成类的模板(这些类具有相同的功能只是类型不同)。它和函数模板的不同是它不能向函数模板一样推断出类型是什么,而是要自己指定类型是什么。比如vector就是一个模板类,T可以是任何类型int ,double ,string或者是自己定义的类类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息,用来代替实参。

(1)定义类模板、实例化类模板

//类模板:
template <typename T>
class Blob
{
public:
	//类模板的成员模板。
	template <typename T1> void output(T thistype, T1 othertype); 
	using size_type = typename vector<T>::size_type;
	//构造函数。
	Blob();
	//列表初始化。
	Blob(initializer_list<T> il):data(make_shared<vector<T>>(il)) { }
	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	//添加和删除元素。
	void push_back(const T &t) { data->push_back(t); }
	void push_back(T &&t) { data->push_back(std::move(t)); }
	void pop_back(const T &t) { data->pop_back(t); }
	void pop_back(T &&t) { data->pop_back(std::move(t)); }
	//元素访问。
	T &back();
	T &operator[](size_type i);
	~Blob(){}
private:
    T *elements;  
    T *first_free;  
	shared_ptr<vector<T>> data;
	//若data[i]无效,则抛出异常。
	void check(size_type i, const string &msg) const;
};

【Note】:
1)一个类模版的每个实例都会形成一个独立的类,与其他实例化的类之间并没有特殊的访问权限。
2)无论何时使用模版都必须提供模版实参。

  实例化时,编译器会重写类模版,将模版参数替换为给定的模版实参。
  在一个类模版中使用另一种模版,通常不会将一个实际的类型(如:int)当作其模版实参,而将模版自己的参数当作被使用模版的实参,比如在一个模版类中使用的vector和shared_ptr都是类模版,我们在使用时,会将T作为模版实参传递给他们。

//类模板的名字不是类型名。
Blob<int> a1{1, 2, 3};//数据类型是int,使用时才被实例化。

(2)类模板的成员函数及实例化、在类代码内简化模板类名的使用、在类模板外使用类模板名

  我们可以在类模版的内部或者外部对类模版的成员函数进行定义,定义在类模版内的成员函数被隐式的声明为inline函数。
  由于类模版的每个实例都有自己版本的成员函数,因此类模版的成员函数具有和模版相同的模版参数,因此,在定义类模版之外的成员函数必须以关键词template开始,后接类模版实参列表(在其返回类型之后还需要加类名和<>参数列表)。

template <typename T> class Blob
{
public:
	Blob();
	void check(size_t,const string&) const;
private:
	shared_ptr<vector<T>> data;
};

template <typename T> Blob<T>::Blob():data(make_shared(vector<T>)){}//类外定义构造函数,类模版的构造函数,无需返回类型,其他和普通成员函数一样。
template <typename T> 
void Blob<T>::check(size_t i,const string& msg)//类外定义成员函数
{
	if (i > data->size())
	{
		throw out_of_range(msg);
	}
}

【Note】:
1)类模版的成员函数实例化:只有在程序使用它时才会被实例化,即使其类模版已经被实例化。

  当我们使用一个类模板类型必须提供模板参数,但有一个例外,在类模版自己的作用域中(即类内),我们可以直接使用模版名而不提供实参(不需要<>这个东西了)而在类外则需要指定模版参数(返回值是模版的类型)

template<typename T>
class blobptr
{
    public:
        blobptr():curr(0){ }
        blobptr(blob<T> &a, std::size_t sz = 0):
            wptr(a.data), curr(sz) { }
        T& operator*()const
        {
            auto p = check(curr, "* error");
            return (*p)[curr];
        }
        blobptr& operator++();//返回blobptr&而不是blobptr&。
        blobptr& operator--();
    private:
        std::shared_ptr<std::vector<T>>
            check(std::size_t i, const std::string &msg);
        std::size_t curr;
        std::weak_ptr<std::vector<T>>wptr;
};

//类外则需要指定模版参数。
template<typename T>  
blobptr<T>& blobptr<T>::operator++()  
{  
    blobptr ret = *this;  
    ++*this;  
    return ret;  
}  
  
template<typename T>  
blobptr<T>& blobptr<T>::operator--()  
{  
    blobptr ret = *this;  
    --*this;  
    return ret;  
} 

(3)模板类和友元、令自己的实例化类型作为友元(c++11)

  当一个类模版包含一个非模版友元,则友元被授权可以访问所有的模版实例,如果友元自身是模版,类可以授权给所有友元模版实例,也可以只授予给定实例。
  类模板与另一个(类或函数)模板间的友好关系最常见的是一对一友好关系。
  如想要所有实例称为友元,友元声明中必须使用与类模版不同的模版参数。

//非类型模板参数。
template <unsigned N>
class bits
{
public:
	bits() = default;
	bits(const string &s) : bit(s) { }
	~bits() =default;
	inline void updata(int s, int b)
	{
		bit.set(s, b);
	}

	//为了让实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
    template<unsigned M>
    friend size_t text(const bits<M> &lb, const bits<M> &rb);  
  	//重载输出操作符。
    template<unsigned M>  
    friend std::ostream &operator<<(std::ostream &os, const bits<M> &b);  

private:
	bitset<N> bit;
};

  一个模板类也可以指定另外一个模板类为友元或者另外一个模板类的特定实例。

#include 

template <typename T>
class C
{

};

template <typename T>
class B
{
    //friend class C;                 //特定类为友元
    template <typename X> friend class C;  //模板类为友元
    template <typename T> friend class C;  //error:本模板类参数列表已经包含T了,友元不可再次包含T,需要换参数,否则会报错显示应藏参数
};

int main()
{
	...
}

  令自己的实例化类型作为友元【C++11】:

#include 

template <typename T, typename X> 
class C
{
    friend T;
};

int main()
{}

  那么我们实例化的时候,实例化类型是A,A就是模板类的友元,A可以是类类型,或者是内置类型。

(4)类模板别名(c++11)、类模板的static成员

  旧标准中,我们只能使用typedef 为特定模板类型定义类型别名。c++11标准中,我们可以为模板类定义模板类型别名,而且多个参数时可以指定参数是T或者是特定类型。

#include 

template <typename T, typename X, typename Z>
class C
{

};

//template参数列表指定参数的个数,但是我们能指定一个或多个特定类型比如TEP2的int
template <typename T, typename X, typename Z> using TEP = C<T, X, Z>;
template <typename T, typename X> using TEP2 = C<T, X, int>;

int main()
{
    C<int, int, int> c;
    TEP<int, int, int> t;
    TEP2<int, int> t2;
}

  当类模板包含static成员时(数据或者是函数),每个类模板的实例都有一个static成员,而且定义类模板的static成员时,前面必须加template <参数列表>。
  在相同的实例中共享static成员,因此元素的值可能发生变化,但是在不同的对象中不互相共享。类模版的static成员有且仅有一个定义。

#include 

template <typename T>
class C
{
    public:
        static void fun(T &a);

    private:
        static int i;
};

template <typename T>
void C<T>::fun(T &a)
{
    std::cout << a << std::endl;
}

//别忘记了int。
template <typename T>
int C<T>::i = 0;

int main()
{
    C<int>c;
}

3、模板参数

  T没有任何的实际意义,只是类型的一个替代。

(1)模板参数与作用域、模板声明、使用类的类型成员

  模版参数的作用于范围在其声明之后,模版的定义或声明结束之前,且其会隐藏外层作用域中相同的名字(在外层使用了typedef T之后,在类中T的含义还是模版参数的含义),且模版参数名不可才参数列表中重复。

typedef double A;
template <typename A, typename B>
void f(A,a,B,b)
{
	A tmp = a;//tmp的类型为A的类型而不是double。
	double B;//错误:重声明模板参数B
}

  声明与定义中的模版参数的名字可以不相同。一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
  我们处理模板的时候,必须让编译器知道名字是否表示一个类型。

T::size_type *p;    //error   typename T::size_type *p 

  我们不知道T是一个名字还是模板的参数列表里的类型,c++编译器默认为名字。所以如果我们像上面想表示为一个类型的话必须显式的指定,在前面添加关键字typename。

(2)默认模板实参、模板默认实参与类模板

  C++11新标准允许默认模版实参(可以为函数提供,早起只能为类模版提供默认模版实参)。

#include 
#include 

//X默认实参是std::less这个函数对象
template <typename T, typename X = std::less<T>>
int compare(const T &v1, const T &v2, X x = X())
{
    if(x(v1, v2))
    {
        std::cout << v1 << " " << v2 << std::endl;
        return -1;
    }
    if(x(v2, v1))
    {
        std::cout << v1 << " " << v2 << std::endl;
        return 1;
    }
    return 0;
}

int main()
{
    //传参数函数对象std::greater
    std::cout << compare(0, 1, std::greater<int>()) << std::endl;
    //使用默认参数  std::less
    std::cout << compare(0, 1) << std::endl;
}

  在我们为类模版的所有参数都提供了默认参数后,加一对空尖括号即可。

#include 

template <typename T = int> 
class C
{
    public:
        C(T v = 0):
            val(v) { }

    private:
        T val;
};

int main()
{
    C<> c;            //使用默认int
    C<double> c2;     //覆盖默认int改用double
}

4、成员模板

  一个类无论是类模版还是普通类,都可以将其成员函数定义为模版,称之为成员模版,但是成员模版不能是虚函数。

//普通(非模板)类的成员模板:
class Debugdelete  
{  
public:  
    Debugdelete(ostream &s = cerr):os(s){}//构造函数。
    template <typename T> void operator()(T *p) const//const表示该函数不会修改类的成员数据。
    {  
        os<<"deleting..."<<endl;//额外信息,我们的删除器可以做用于任何版本类型。
        delete p;//接受一个指针作为参数,并且删除该指针。
    }  
private:  
    ostream &os;//私有成员为一个输出流。
};

double *p = new double;//新分配一个double对象。
Debugdelete d;//创建一个删除器对象。
d(p);//调用定义的模版函数operator (),对p进行释放。
//可以对类型的删除器进行重载我们自己定义的版本,在尖括号中给出删除器的类型,
//并提供一个这个类型的对象给它的构造函数即可。
unique_ptr<int,Debugdelete> m(new int,Debugdelete());
shared_ptr<int> n(new int,Debugdelete());


//类模板的成员模板
template <typename T>  
class C  
{  
    public:  
        template <typename X>C(X b, X e);  
      
    private:  
        std::vector<T>ivec;  
};  
//同时为类模板和成员模板提供模板参数列表。
template <typename T>       //类的参数列表  
template <typename X>       //构造函数的参数列表  
C<T>::C(X b, X e)  
{  
  ...
}  

  实例化一个类模板的成员函数模板,类模板必须明确指出类型,函数模板根据参数推断出类型,如上面main函数里面的实例。

5、控制实例化、效率与灵活性

  对于模版使用时才会被实例化,会产生一个问题:相同的实例可能会出现在多个对象文件中,这时候每个文件都会有一份实例化的副本,这无疑造成了很大的额外开销,所以在C++11新标准下,我们可以使用显示实例化以避免这样的开销,所有的模版参数会被替换为模版实参:

extern template class Blob<string>;        //实例化class声明
template int compare(const int&, const int&);    //实例化compare函数定义

  当编译器遇到extern模版声明时,不会在本文件中生成实例化代码。将一个实例化声明为extern,就表示承诺在程序其他位置有该实例化的一个非extern定义,只能有一个定义!
【Note】:
1)对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。

  一个类模版的实例化定义会实例化该模版的所有成员,包括内联函数成员,因为我们也不知道程序会使用那些成员函数,所以我们必须将其全部实例化,这就要求在实例化定义中,所用类型必须能作用于模版的所有成员函数。

  shared_ptr删除器:在运行时绑定的删除器。在一个shared_ptr的生存期中,我们可以随时改变其删除器的类型。
  unique_ptr删除器:在编译时绑定的删除器。unique_ptr的删除器是unique_ptr的一部分。unique_ptr有两个模板参数,一个表示它所管理的指针,另一个表示删除器的类型,在编译时绑定,所以没有运行开销

6、类模板和成员模板demo:

#include 
using namespace std;
//成员模板:
class Debugdelete  
{  
public:  
    Debugdelete(ostream &s = cerr):os(s){}//构造函数。
    template <typename T> void operator()(T *p) const//const表示该函数不会修改类的成员数据。
    {  
        os<<"deleting..."<<endl;//额外信息,我们的删除器可以做用于任何版本类型。
        delete p;//接受一个指针作为参数,并且删除该指针。
    }  
private:  
    ostream &os;//私有成员为一个输出流。
};

//类模板:
template <typename T>
class Blob
{
public:
	//类模板的成员模板。
	template <typename T1> void output(T thistype, T1 othertype); 
	using size_type = typename vector<T>::size_type;
	//构造函数。
	Blob();
	//列表初始化。
	Blob(initializer_list<T> il):data(make_shared<vector<T>>(il)) { }
	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	//添加和删除元素。
	void push_back(const T &t) { data->push_back(t); }
	void push_back(T &&t) { data->push_back(std::move(t)); }
	void pop_back(const T &t) { data->pop_back(t); }
	void pop_back(T &&t) { data->pop_back(std::move(t)); }
	//元素访问。
	T &back();
	T &operator[](size_type i);
	~Blob(){}
private:
    T *elements;  
    T *first_free;  
	shared_ptr<vector<T>> data;
	//若data[i]无效,则抛出异常。
	void check(size_type i, const string &msg) const;
};

template <typename T>
void Blob<T>::check(size_type i, const string &msg) const
{
	if (i >= data->size())
		throw out_of_range(msg);
}

template <typename T>
T &Blob<T>::back()
{
	check(0,"back on empty Blob");
	return data->back();
}

template <typename T>
T &Blob<T>::operator[](size_type i)
{
	//如果i太大,抛出异常。
	check(i,"out of range");
	return (*data)[i];
}

//类模板的模板参数列表在成员模板参数列表之前。
template <typename T>  
template <typename T1>  
void Blob<T>::output(T thistype, T1 othertype)  
{  
    cout<< "thistype is " << typeid(thistype).name()
        <<", othertype is " << typeid(othertype).name() << endl;  
}  

int main(int argc, char const *argv[])
{

	//类模板。
	Blob<int> a1{1, 2, 3};//数据类型是int,使用时才被实例化。
	a1.push_back(4);
	for (size_t i = 0; i != a1.size(); ++i)
		cout << a1[i] << " ";
	Blob<string> str{"ab", "cd", "ef"};
	cout << endl << str[1] << endl;
	a1.output(10,true);

	//成员模板:
    double *p = new double;//新分配一个double对象。
    Debugdelete d;//创建一个删除器对象。
    d(p);//调用定义的模版函数operator (),对p进行释放。
    //可以对类型的删除器进行重载我们自己定义的版本,在尖括号中给出删除器的类型,
    //并提供一个这个类型的对象给它的构造函数即可。
    unique_ptr<int,Debugdelete> m(new int,Debugdelete());
    shared_ptr<int> n(new int,Debugdelete());
    cin.get();
	return 0;
}

【C++ Primer】模板与泛型编程_第2张图片

二、模板实参推断

  从函数实参到模版实参的过程被称为模版实参推断,在这个过程中,可能会发生类型转换。

1、类型转换与模板实参推断

  在模版推断的过程中,编译器根据函数调用的实参类型来寻找模版实参,用这些模版实参生成的函数版本与给定的函数调用匹配。
  只有两种情况会发生类型转换:

  • 非const对象的引用或者指针到const对象的引用或者指针(const到非const是不合法的)(顶层const会被忽略)。
  • 若函数参数不是引用的类型,则可以进行函数或者数组的指针类型转换,我们可以用函数实参或者数组实参转化为对应指针。
    【Note】:
    1)将实参传递给代模板类型的参数时,能够自动类型转换的只有const转换及数组或函数到指针的转换。如果想要传递不同的参数,则我们必须自己定义那种不同的模板类型。
    2)如果函数模板的参数不是模板则可以正常进行转换。
    3)算术类型转换不能支持模版函数的类型转换,若我们想要两个不同类型的参数,定义两个不同的模版参数即可。
/***********************
*类型转换与模板类型参数。
************************/
template <typename T>
T fobj(T a, T b)
{
	cout << a << ", " << b << endl;
}

template <typename T>
T fref(const T &a, const T &b)
{
	cout << a << ", " << b << endl;
}

template <typename A, typename B>
void fcpe(const A &a, const B &b)
{
	cout << a << ", ";
	cout << b << endl;
}

template <typename T>
ostream &fprt(ostream &os, const T &t)
{
	os << t << endl;
}

template <typename T>
T fcn(T a, int b)
{
	cout << a << ", " << b << endl;
}

string s1("a string");
const string s2("anthor string");
fobj(s1, s2);
fref(s1, s2);//将s1转换为const是允许的。
int a[10], b[10];
fobj(a, b);//数组名转换为指针,打印地址。
fcpe(0.1, 10);//第一个为float,第二个为int被提升为float。
fprt(cout, 10);//严格匹配。
char c = 'b';
fcn(c, 'c');//char提升为int。字符c表示的ASCII码。

2、函数模板显式实参

  在某些情况下,编译器无法推断出模板实参的类型。在其他一些情况下,我们希望允许用户控制模板实例化。通常有两个原因:

  • 模版实参作为函数的返回类型,传入的函数实参不能推断出函数的返回类型。
  • 我们希望可以参数指定类型,进行类型转换。

  显式模版实参,位于函数名后,参数列表之前,注意其函数模版实参是按从左至右顺序进行匹配的,之后尾部的参数可以忽略(忽略的前提是可以通过传入的实参进行推断),所以不要将最后的参数作为返回类型,否则需要将所有的模版实参进行显式的初始化:

/**************
*函数模板显示实参。
***************/
template <typename T1, typename T2, typename T3>
T1 sum(T2 t2, T3 t3)
{
	T1 p = t2 + t3;
	return p;
}

long lng = 1000000;
auto val2 = sum<long long>(1,lng);//指定显式模板实参。
cout << val2 << endl;

3、尾置返回类型与类型转换

  一些情况下,要求显式指定模板实参会给用户添加额外负担,且不会带来什么好处。此时我们可以使用尾置返回类型。

/***********************
*尾置返回类型与类型转换。
************************/
template <typename T>
auto sum2(const T &a,const T &b) ->decltype(a+b)//将函数的返回类型指定为a+b的类型。
{
    return a + b;
}

int a3 = 566669;
int b3 = 59;
cout << sum2(a3, b3) << endl;//处理足够大小的数相加。

4、函数指针和实参推断

  我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时。编译器会用指针的类型来推断模板。

/*******************
*函数指针和实参推断。
********************/
template <typename T>
int compare(const T &a, const T &b)
{
    return a < b ? 1 : 0 ;
}

int (*p)(const int &, const int &) = compare;
cout << p(1, 2) << endl;//p是指向函数的指针,使用指针来推断模板实参为int。

【Note】:
1)当参数是一个函数模板实例的地址时,程序上下文必须满则:对每个模板参数能唯一确定其类型或值。

5、模板实参推断和引用

(1)从左值引用函数参数推断类型

  当一个函数类型参数是普通的左值引用时,规定只能传递给它一个左值(一个变量或一个引用的表达式),实参可以是const类型也可以不是。
  如果实参是const类型,T将会被推断是const类型。如果函数类型参数是const&T时,我们可以传递任何类型的实参,一个对象,一个临时对象或者字面值常量。

(2)从右值引用函数参数推断类型

  我们可以传递一个右值,推断规则类似左值引用。左值不能绑定在右值引用上,但是有两个例外规则。

  • 当我们将一个左值传递给函数的右值引用参数时,编译器推断模板类型参数为实参的左值引用类型。注意是引用。比如传递给模板函数参数T &&一个int i,推断出来的类型就是int &。
  • 如果我们间接创建一个引用的引用就会形成引用折叠,引用折叠在大部分情况下会形成一个普通的左值引用,但是在右值引用的引用情况下会生成右值引用。
      X& &, X& &&和X&& &都折叠成X&
      X&& &&折叠成X&&
    【Note】:
    1)引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
    2)若模版函数参数为右值引用,则可以绑定一个左值,且如果传入实参是左值的话,编译器推断出的模版实参类型将是一个左值的引用,且函数参数将会被实例化一个普通的左值引用类型。
/*******************
*模板实参推断和引用。
********************/
//当模版参数类型是一个左值引用时(T&),只能传递给它一个左值(一个变量、一个返回引用类型的表达式),
//实参可以是const类型,如果实参是const,T将会被推断为const类型。
template <typename T>
void f4(T &t)
{
	cout << t << endl;
}

template <typename T>
void f4(T &&t)
{
	cout << t << endl;
}

f4(b3);//实参是左值。
f4("f4");//实参是右值。
f4(b3);//函数参数是指向模板类型实参的右值引用,则可以被绑定到左值上。---引用折叠。

6、 理解std::move

  标准库move函数是使用右值引用模版的一个很好的例子,我们不能将一个右值的引用绑定到一个左值上,但是通过move函数就可以完成该操作,move可接收左值也可以接收右值。

/**************
*理解std::move。
***************/
//通过引用折叠,T &&可以与任意类型的实参匹配,左值和右值皆可。
template <typename T>
typename remove_reference<T>::type &&move(T &&t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

string s5 = std::move(string("Hi"));

7、 转发

  某些函数需要将一个或多个实参连同类型传递给其他的函数,这种情况下我们需要保证实参的类型。
  如果一个函数参数是指向模版类型参数的右值引用(T&&),则它对应的实参的const属性和右值/左值属性将得到保持,但此方法不能用于接受右值引用的函数。
  当用于一个指向模版参数类型的右值引用参数时(T&&),forward()会保持实参类型的每个细节,头文件为utility,后必须加显式模版参数。

/*****
*转发。
******/
template <typename F, typename IT1, typename IT2>  
void flip(F f, IT1 &&t1, IT2 &&t2)//参数为右值引用,且使用forward传递参数,可以保证参数的每个细节不变。
{  
    f(std::forward<IT2>(t2), std::forward<IT1>(t1));
}  

void f(int a, int &b)
{
    cout << a << " " << ++b << endl;
}

int i6 = 5;  
flip(f, i6, 42);

8、 本节demo:

#include 
using namespace std;

/***********************
*类型转换与模板类型参数。
************************/
template <typename T>
T fobj(T a, T b)
{
	cout << a << ", " << b << endl;
}

template <typename T>
T fref(const T &a, const T &b)
{
	cout << a << ", " << b << endl;
}

template <typename A, typename B>
void fcpe(const A &a, const B &b)
{
	cout << a << ", ";
	cout << b << endl;
}

template <typename T>
ostream &fprt(ostream &os, const T &t)
{
	os << t << endl;
}

template <typename T>
T fcn(T a, int b)
{
	cout << a << ", " << b << endl;
}

/*****************
*函数模板显示实参。
******************/
template <typename T1, typename T2, typename T3>
T1 sum(T2 t2, T3 t3)
{
	T1 p = t2 + t3;
	return p;
}

/***********************
*尾置返回类型与类型转换。
************************/
template <typename T>
auto sum2(const T &a,const T &b) ->decltype(a+b)//将函数的返回类型指定为a+b的类型。
{
    return a + b;
}

/*******************
*函数指针和实参推断。
********************/
template <typename T>
int compare(const T &a, const T &b)
{
    return a < b ? 1 : 0 ;
}

/*******************
*模板实参推断和引用。
********************/
//当模版参数类型是一个左值引用时(T&),只能传递给它一个左值(一个变量、一个返回引用类型的表达式),
//实参可以是const类型,如果实参是const,T将会被推断为const类型。
template <typename T>
void f4(T &t)
{
	cout << t << endl;
}

template <typename T>
void f4(T &&t)
{
	cout << t << endl;
}

/**************
*理解std::move。
***************/
//通过引用折叠,T &&可以与任意类型的实参匹配,左值和右值皆可。
template <typename T>
typename remove_reference<T>::type &&move(T &&t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

/*****
*转发。
******/
template <typename F, typename IT1, typename IT2>  
void flip(F f, IT1 &&t1, IT2 &&t2)//参数为右值引用,且使用forward传递参数,可以保证参数的每个细节不变。
{  
    f(std::forward<IT2>(t2), std::forward<IT1>(t1));
}  

void f(int a, int &b)
{
    cout << a << " " << ++b << endl;
}

int main(int argc, char const *argv[])
{

	/***********************
	*类型转换与模板类型参数。
	************************/
	string s1("a string");
	const string s2("anthor string");
	fobj(s1, s2);
	fref(s1, s2);//将s1转换为const是允许的。
	int a[10], b[10];
	fobj(a, b);//数组名转换为指针,打印地址。
	fcpe(0.1, 10);//第一个为float,第二个为int被提升为float。
	fprt(cout, 10);//严格匹配。
	char c = 'b';
	fcn(c, 'c');//char提升为int。字符c表示的ASCII码。
	
	/*****************
	*函数模板显示实参。
	******************/
	long lng = 1000000;
	auto val2 = sum<long long>(1,lng);//显式模板实参。
	cout << val2 << endl;

	/***********************
	*尾置返回类型与类型转换。
	************************/
	int a3 = 566669;
    int b3 = 59;
    cout << sum2(a3, b3) << endl;//处理足够大小的数相加。

	/*******************
	*函数指针和实参推断。
	********************/
    int (*p)(const int &, const int &) = compare;
    cout << p(1, 2) << endl;//p是指向函数的指针,使用指针来推断模板实参为int。

	/*******************
	*模板实参推断和引用。
	********************/
    f4(b3);//实参是左值。
    f4("f4");//实参是右值。
    f4(b3);//函数参数是指向模板类型实参的右值引用,则可以被绑定到左值上。---引用折叠。

	/**************
	*理解std::move。
	***************/
    //string s5 = std::move(string("Hi"));

	/*****
	*转发。
	******/
    int i6 = 5;  
    flip(f, i6, 42);

	system("pause");
	return 0;
}

三、重载与模板

   函数模版可以被另一个模版或者普通非模版函数重载:相同名字函数,具有不同数量或者类型的参数。
  若模版被重载,则函数的匹配会发生变化:

  • 匹配过程中,候选函数包括所有的模版实参推断成功的模版实例。
  • 可行函数按类型转换来排序,需要类型转换的排在不需要转换的后面。
  • 若有多个函数提供同样的匹配:若是非模版函数,选择非模版函数。若没有非模版函数,则那个模版的特例高,选哪个,否则的话,此调用会产生歧义。
    【Note】:
    1)若多个模版皆为精确匹配,正常情况下无法区分,但是更特例化的的模版排在前面,比如const T&和T*p,T *p更加特例化。
    2)对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
    3)在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。
#include 
#include 
#include 
#include 
using namespace std;

//所有模版的声明都需要在函数定义前声明。
template<typename T> void f(T t)  
{  
    cout<<"f:T t:"<<t<<endl;  
}  
  
template<typename T> void g(T *t)  
{  
    cout<<"g:T *t:"<<t<<endl;  
}  
  
template<typename T> void g(T t)  
{  
    cout<<"g:T t:"<<t<<endl;  
}  
  
template<typename T> void f(const T *t)  
{  
    cout<<"f:const T*:"<<t<<endl;  
}  

int main(int argc, char** argv)
{
    int i = 42,*p = &i;
    const int ci = 0, *p2 = &ci;

    g(42);//调用g(T t)
    g(p);//调用g(T *t)
    g(ci);//调用g(T t)
    g(p2);//调用g(T *t)
    f(42);//调用f(T t)
    f(p);//调用f(T t)
    f(ci);//调用f(T t)
    f(p2);//调用f(const T *t)
    system("pause");
    return 0;
}

四、可变参数模板(c++11)

  可变函数模版就是指一个接受可变数目参数的模版函数或者模版类,可变数目的参数被称为参数包,分为两种:模版参数包,表示零个或多个模版参数,函数参数包,表示零个或多个函数参数。利用一个省略号来表示一个模版参数或者函数参数为一个包。
  当我们需要知道包中有多少元素时,我们可以使用sizeof…()运算符(注意有省略号),值求出参数的数目。
  与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。

template<typename T, typename... U> 
void Foo(const T &, const U &...dest)  
{  
    //U表示一个模版参数包,dest表示一个函数参数包。
    //当我们需要知道包中有多少元素时,我们可以使用sizeof...()运算符(注意有省略号),值求出参数的数目。
    cout << sizeof...(U) << endl;  
    cout << sizeof...(dest) << endl;  
}  

1、编写可变参数函数模板

  以前说过initializer_list定义一个可变数目参数的函数,但是类型是指定的。
  当我们不知道参数的数目和参数的类型时,可变参数函数是很有用的。可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
  当定义可变参数版本的函数时,非可变参数版本的声明必须在作用域中,否则,可变参数版本会无限递归。好的做法是都在最开始声明。

/*********************
*编写可变参数函数模板。
**********************/

template<typename T>
//非可变参数版本,递归的最后一次调用会选择该版本,因为比较特例化。
//必须声明在可变参数模板的定义之前,否则可变参数模板会无限递归。
std::ostream &print(std::ostream &os, const T &t)
{    
    return os << t;    
}    
template<typename T, typename... Args>    
std::ostream &print(std::ostream &os,const T &t,const Args&... args)    
{    
    os << t << ", ";
    return print(os, args...); 
}    

print(cout, 42) << endl;
print(cout, 42, a) << endl;
print(cout, 42, a, q, "hello", 'w') << endl;

2、包扩展

  对于一个参数包,我们还可以对其进行参数扩展,即将一个包分解为其构成元素,我们通过在模式的右边放一个省略号…来出发扩展操作。
  我们还可以对作为参数的函数进行扩展,但注意省略号的位置,不是对函数参数的扩展。

//打印我们不能处理的类型。
template<typename T>    
std::string debug_rep(const T &s)    
{    
    std::ostringstream ret;    
    ret << s;    
    return ret.str();//返回ret绑定的string的一个副本。
} 

//打印指针的值,后跟指针指向的对象。
template<typename T>    
std::string debug_rep(T *p)    
{    
    std::ostringstream ret;    
    std::cout << "point:" << p;//打印指针本身的值。 
    if (p)    
        ret << " " << debug_rep(*p);    
    else    
        ret << "point is NULL!";    
    return ret.str();    
} 

//打印双引号包围的string。
std::string debug_rep(const std::string &s)    
{    
    return '"' + s + '"';    
}    

//将字符指针转换为string,并调用string版本的debug_rep。
std::string debug_rep(char *p)    
{    
    return debug_rep(std::string(p));    
}    
    
std::string debug_rep(const char *p)    
{    
    return debug_rep(std::string(p));    
}    

//包扩展:参数包中每个元素调用debug_rep。
template<typename... Args>    
std::ostream &errorMsg(std::ostream &os, const Args... args)    
{    
    return print(os, debug_rep(args)...);    
}    

string str = "c++";    
errorMsg(cout, str, "primer", 4, 8.6, '5'); 

3、本节demo:

#include 
using namespace std;  

template<typename T, typename... U> 
void Foo(const T &, const U &...dest)  
{  
    //U表示一个模版参数包,dest表示一个函数参数包。
    //当我们需要知道包中有多少元素时,我们可以使用sizeof...()运算符(注意有省略号),值求出参数的数目。
    cout << sizeof...(U) << endl;  
    cout << sizeof...(dest) << endl;  
}  

/*********************
*编写可变参数函数模板。
**********************/

template<typename T>
//非可变参数版本,递归的最后一次调用会选择该版本,因为比较特例化。
//必须声明在可变参数模板的定义之前,否则可变参数模板会无限递归。
std::ostream &print(std::ostream &os, const T &t)
{    
    return os << t;    
}    
template<typename T, typename... Args>    
std::ostream &print(std::ostream &os,const T &t,const Args&... args)    
{    
    os << t << ", ";
    return print(os, args...); 
}    

//打印我们不能处理的类型。
template<typename T>    
std::string debug_rep(const T &s)    
{    
    std::ostringstream ret;    
    ret << s;    
    return ret.str();//返回ret绑定的string的一个副本。
} 

//打印指针的值,后跟指针指向的对象。
template<typename T>    
std::string debug_rep(T *p)    
{    
    std::ostringstream ret;    
    std::cout << "point:" << p;//打印指针本身的值。 
    if (p)    
        ret << " " << debug_rep(*p);    
    else    
        ret << "point is NULL!";    
    return ret.str();    
} 

//打印双引号包围的string。
std::string debug_rep(const std::string &s)    
{    
    return '"' + s + '"';    
}    

//将字符指针转换为string,并调用string版本的debug_rep。
std::string debug_rep(char *p)    
{    
    return debug_rep(std::string(p));    
}    
    
std::string debug_rep(const char *p)    
{    
    return debug_rep(std::string(p));    
}    

//包扩展:参数包中每个元素调用debug_rep。
template<typename... Args>    
std::ostream &errorMsg(std::ostream &os, const Args... args)    
{    
    return print(os, debug_rep(args)...);    
}    

int main(int argc, char** argv)  
{  
    int i = 2;  
    string q = "54";  
    double a = 3.14;  
    Foo(i, q, 45, a);  
    Foo(i, q, 45);  
    Foo(i, q);  
    Foo(i);

    /*********************
    *编写可变参数函数模板。
    **********************/
    print(cout, 42) << endl;
    print(cout, 42, a) << endl;
    print(cout, 42, a, q, "hello", 'w') << endl;

    /*******
    *包扩展。
    ********/
    string str = "c++";    
    errorMsg(cout, str, "primer", 4, 8.6, '5');    

    return 0;  
}  

【C++ Primer】模板与泛型编程_第3张图片

五、模板特例化

  当我们不能(或者不希望)使用模版版本时,我们可以定义类模版或者函数模版的一个特例化版本:比如说函数模版中的处理不适用于未定义<运算符(指针类型)的情况,我们就可以特例化一个版本已使用特殊情况。一个特例化版本就是模版的一个独立的定义,在其中一个或者多个参数被特定为指定的类型
  在为函数模版特例化时,必须为函数模版的每个模版参数提供实参,尖括号中的模版参数去掉,但是必须提供实参。
【Note】:
1)特例化的本质是实例化一个模版,而非重载,因此特例化不影响函数的重载,它不是一个非模版的独立函数。
2)模版及其特例化版本应该定义在一个头文件中,所有同名的模版的声明应该放在前面,然后是这些特例化的声明。
3)类模版也可以进行特例化,需要在原模版定义所在的命名空间中特例化。

#include    
class Sales_data    
{    
    std::string bookNo;    
    unsigned units_sold;    
    double revenue;    
    double avg_price()const { return units_sold ? revenue / units_sold : 0; }    
public:    
    Sales_data(const std::string &s=std::string(), unsigned n = 0, double p = 0) :bookNo(s), units_sold(n), revenue(p) {}    
    Sales_data(std::istream &is);    
    std::string isbn()const { return bookNo; }    
    Sales_data &operator+=(const Sales_data &s);    
    friend std::hash<Sales_data>;    
    friend std::ostream &operator<<(std::ostream &os, const Sales_data &s);    
    friend std::istream &operator>>(std::istream &is, Sales_data &s);    
    friend bool operator==(const Sales_data &ls, const Sales_data &rs);    
    friend Sales_data operator+(const Sales_data &ls, const Sales_data &rs);    
    friend std::ostream &print(std::ostream &os, const Sales_data &s);    
    friend std::istream &read(std::istream &is, Sales_data &s);    
};    
bool operator!=(const Sales_data &ls, const Sales_data &rs);    
Sales_data add(const Sales_data &ls, const Sales_data &rs);    
    
namespace std    
{    
    template<> //特例化一个版本,模板参数为Sales_data。
    struct hash<Sales_data>    
    {    
    	//用来散列一个无序容器的类型必须定义以下类型。
        typedef size_t result_type;    
        typedef Sales_data argument_type;    
        size_t operator()(const Sales_data &s)const {   
        	//对哈希值进行异或运算。 
            return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue);    
        }    
    };    
}    

Sales_data::Sales_data(std::istream &is)    
{    
    is >> *this;    
}    
    
Sales_data &Sales_data::operator+=(const Sales_data &s)    
{    
    units_sold += s.units_sold;    
    revenue += s.revenue;    
    return *this;    
}    
    
std::ostream &operator<<(std::ostream &os, const Sales_data &s)    
{    
    os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg_price();    
    return os;    
}    
    
std::istream &operator>>(std::istream &is, Sales_data &s)    
{    
    double price;    
    is >> s.bookNo >> s.units_sold >> price;    
    if (is)    
        s.revenue = s.units_sold*price;    
    else    
        s = Sales_data();    
    return is;    
}    
    
bool operator==(const Sales_data &ls, const Sales_data &rs)    
{    
    return ls.bookNo == rs.bookNo&&ls.units_sold == rs.units_sold&&ls.revenue == rs.revenue;    
}    
bool operator!=(const Sales_data &ls, const Sales_data &rs)    
{    
    return !(ls == rs);    
}    
    
Sales_data operator+(const Sales_data &ls, const Sales_data &rs)    
{    
    Sales_data temp = ls;    
    temp += rs;    
    return temp;    
}    
    
Sales_data add(const Sales_data &ls, const Sales_data &rs)    
{    
    Sales_data temp = ls;    
    temp += rs;    
    return temp;    
}    
    
std::ostream &print(std::ostream &os, const Sales_data &s)    
{    
    os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg_price();    
    return os;    
}    
    
std::istream &read(std::istream &is, Sales_data &s)    
{    
    double price;    
    is >> s.bookNo >> s.units_sold >> price;    
    s.revenue = s.units_sold*price;    
    return is;    
}    

int main()    
{    
    std::unordered_multiset<Sales_data> mset;    
    Sales_data sd("Bible", 10, 0.98);    
    
    mset.emplace(sd);    
    mset.emplace("C++ Primer", 5, 9.99);    
    
    for (const auto &item : mset)    
        std::cout << "the hash code of " << item.isbn()    
        << ":\n0x" << std::hex << std::hash<Sales_data>()(item)    
        << "\n";    
    system("pause");    
    return 0;    
}    

【C++ Primer】模板与泛型编程_第4张图片

你可能感兴趣的:(#,C++进阶)