模板是一个非常强大的C++功能,STL的各种组件也是基于模板的。所以,无论是写程序了,还是读程序,都有必要了解一下C++的模板。
关于什么是模板或者模板的基本定义,这里就不讲述了,本篇文章主要罗列出在使用模板过程中的一些问题和模板一些令人头疼的语法,并配合简单的demo,如果你只是希望查阅语法或者了解一些知识点,这篇文章可能会帮到你。
声明:使用了using namespace std。对于应该包含进来的头文件,不再显示的声明。文中所有demo均经过测试。本文章基于《C++ Primer Plus》和《C++ Prime》。
目录
模板的基本声明和定义
模板的声明
template <typename T> int compare (T t1, T t2);
template <typename T> class compare;
定义一个模板函数
template <typename T>
int compare(T & t1, T & t2)
{
if(t1 > t2)
return 1;
if(t1 == t2)
return 0;
if(t1 < t2)
return -1;
}
定义一个模板类
template <typename T>
class compare
{
private:
T _val;
public:
explicit compare(T & val) : _val(val) { }
explicit compare(T && val) : _val(val) { }
bool operator==(T & t)
{
return _val == t;
}
};
模板参数作用域
就如同其他的函数参数一样,或者是变量一样,就是普通的作用域规则。
using T = int;
T a = 10;
template <typename T> class A;
模板声明里的T不是上面的int而是模板参数。
template <typename T> class A
{
U val; //error
template<typename U> class B;
};
关于模板工作原理
模板定义并不是真正的定义了一个函数或者类,而是编译器根据程序员缩写的模板和形参来自己写出一个对应版本的定义,这个过程叫做模板实例化。编译器成成的版本通常被称为模板的实例。编译器为程序员生成对应版本的具体过程。类似宏替换。
模板类在没有调用之前是不会生成代码的。
由于编译器并不会直接编译模板本身,所以模板的定义通常放在头文件中。
非类型模板参数
顾名思义,模板参数不是一个类型而是一个具体的值——这个值是常量表达式。
当一个模板被实例化时,,非类型参数被一个用户提供的或者编译器推断出的值所代替。正因为模板在编译阶段编译器为我们生成一个对应的版本,所以其值应该能够编译时确定,那么他应该是一个常量或者常量表达式。
有一句话说:C++的强大在于他的编译器强大,下面这个例子就是很好的说明。
template <size_t N, size_t M>
int str_compare(const char (&str1)[N], const char (&str2)[M])
{
return strcmp(str1,str2);
}
使用方法
str_compare("hello","nihao")
为什么???我们甚至没有用<>来传递模板参数。这是因为编译器在编译阶段已经帮助我们计算好了应该开辟多大空间的数组。我们也可以指定长度。N,M只是隐式的传入进去。
编译器也可以自动帮助我们推断参数时什么类型,从而不用显示的调用模板函数,对于上面的compare函数,我们可以这样调用,前提时保证参数类型相同。
compare(10,20);
非类型模板参数的范围
整形,指针或者左值引用都是一个非类型模板参数。
我们可以想到,对于指针或者引用,应当保证实参必须具有静态的生存期,保证其不会被释放。
inline和constexp
放在模板之后,函数之前即可
template <typename T>
inline int compare(T t1, T t2);
在模板类中使用模板类
这个应该很好理解,根据自己的需求,我们可以这样定义
template <typename T>
class A
{
private:
vector<T> vec;
};
也可以这样定义
template <typename T>
class B
{
private:
vector<int> vec;
};
友元与模板类
通过上面编译器为模板生成具体代码的原理可以看出这样有什么不同
template <typename N>
class C
friend A<N>;
friend B<int>;
由于具体的原理类似宏替换,每个对应的C<N>都有友元A<N>和B<int>、
即有这样友元关系C<int> A<int> B<int>, C<string> A<string> B<string>以此类推。
还有这样的模板友元——所有的实例化都是其友元
template <typename N>
class C
template <typename T> friend class D;
但是没有这样的写法
template <typename T> friend class D<T>;
或者这样的写法
template <typename T> friend D<T>;
模板允许模板参数为自己的友元
首先说明,模板允许内置类型为自己的友元。
friend int;
这样写是完全正确的,但是实际上有什么意义呢?
还是有意义的,我们可以这样写
template <typename T>
class People
{
friend T;
};
这样就保证了在传入内置类型的时候不会有错误。
默认模板实参
用法和函数的默认参数基本相同
template <typename T = int> class A;
默认的情况下T就是int
A<> a; // T is int
: : 二义性的解决
对于普通类的:: ,我们可以知道它究竟是一个类还是一个静态成员,就像下面这样。
string::size_type a;
string::npos;
对于模板类来说,我们还是知道表达的是什么,但是已经说过了,模板类在没有 调用之前不会生成代码,这可坏了。对于T::mem,究竟是什么呢?是静态成员?还是一个类型的typedef?
对于这个问题,使用typename修饰。
当我们希望通知编译器一个名字表示一个类型时,使用且必须使用关键字typename,来表示其是一个类型。
于是,我们可以写出这样的代码。
template <typename T>
typename T::val_typefunc ();
表的不是一个静态数据成员而是一个类型。
或者这样的代码
typedef typename T::mem s_type;
表示s_type是一个类型的别名而不是数据成员的别名。
如果转到string::size_type的定义,可以看见他是一个typename 的 typedef。
类模板成员函数
本质上就是个函数,只要掌握了模板的工作原理,我们我们就可以轻松的写出类模板成员函数。
class Math
{
public:
template <typename N> inline static N sqrt(N);
};
template<typename N>
N Math::sqrt(N val)
{
return val * val;
}
首先来一点一点解析
这是一个模板函数,返回值为N类型,所以,模板语法写在前面,让编译器知道应该返回类型,紧接着就是返回类型,返回类型同上都是写在比较靠前的位置。接着就是函数的标签。
对于定义来说,应该知道是哪个类下的函数,所以和普通的方法一样加上一个作用域即可。
假如把类写成这样呢?
template <typename N>
class Math
{
public:
inline static N sqrt(N);
};
那方法的定义应该是写成这样的。
template<typename N>
N Math<N>::sqrt(N val)
{
return val * val;
}
这里就可以看出
前面说到的,模板不是一个具体的类,而是根据这个模板编译器生成对应的版本。
对于每一个版本,都是不同的类。就像重载函数一样,即便参数个数和函数的具体算法完全一样,但类型不同他们也是不同的函数,只不过函数名相同而已。
那么就应该可以得到每个版本的类都对应的一个相应版本的静态成员。所以Math<N>::这样写也就很好理解了。
类模板的成员模板
我已经不知道用什么语言来下面的代码了。但是我们知道了一些事情。
无论是定义还是声明,模板语法的优先级是最高的,不同模板的优先级又根据其声明顺序来判断,其次是函数修饰,然后是返回值。根据这个原则我们可以轻松的解析这个函数。
template <typename T> class A
{
public:
template <typename It> A<T> sum(It _begin, It _end);
};
template <typename T> //最外层模板
template <typename It> //内层模板
A<T> //返回值
A<T>::sum(It _begin, It _end)//函数标签
{} //算法实现
//不妨写的更美观一点
template <typename T>
template <typename It>
A<T> A<T>::sum(It _begin, It _end){
}
注意:上面的代码和下面的代码写的足够复杂,下面的代码对其进行一些小小的修改。
具体的用法,虽然下面的例子看起来有些造作,但是还是能说明一些问题的
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
template <typename T> class A
{
private:
vector<T> vec;
public:
template <typename It> T sum(It _begin, It _end);
A(initializer_list<T> initlist)
{
for(auto it = initlist.begin();it != initlist.end();++it)
{
vec.push_back(*it);
}
}
typename vector<T>::iterator begin()
{
return vec.begin();
}
typename vector<T>::iterator end()
{
return vec.end();
}
};
template <typename T>
template <typename It>
T A<T>::sum(It _begin, It _end)
{
T tot ;
memset(&tot,0,sizeof (T));
while(_begin != _end)
{
tot += *_begin++;
}
return tot;
}
int main()
{
A<int> a {1,2,3,4};
cout << a.sum(a.begin(),a.end());
return 0;
}
虽然这样的语法很是令人头疼,但是多用即可熟练,或者说使用类型别名来避免这样的问题,并且最好不要把学习精力放在语法上——在没有熟悉语法之前。
实例化优化
当模板被调用时才会被编译,那么就会存在这样一种情况——相同地实例化可能出现在多个文件对象中。当两个或多个独立编译地源文件适用了相同地模板,并提供了相同地模板参数时,每个文件中就都会有该模板的一个实例。
为了解决这种问题,我们可以控制显示实例化。具体的做法如下
用关键字extern显示的实例化声明
extern template class A<string>; //声明
template int compare(const int &, const int &); //定义
将一个实例化声明为extern就表示承诺在程序的其他位置有该实例化的一个非extern声明(定义)。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何用此实例化版本的代码之前。
解释的来说:因为文件其他处已将有一个实例化——编译器生成好的或者是自己定义的,由于编译单元为.cpp文件,所以在一个文件中实例号的代码并不能用于另一个文件,这就显着很捞。而extern正是解决这个问题的。
类型转换和参数推断
与非普通地类型转换一样,模板类在传递模板参数的时候也会进行相应的转换,只不过这种转换增添了更多的规则,参数推断和类型转换的关系是非常紧密的。
类型转换这里的问题如果想要清楚的了解,那恐怕是非常可怕的,我有时候在想,通过这么多的转换规则 ,我们就可见一斑的看出C++的设计是多么的巧妙。虽然这的知识点很乱,但其实只要抓住隐藏在这背后的观念就能清晰的对付各种转换了。
其中一个规则是:如果能够进行安全转换,那么编译器可以隐式转换
最经典的一个例子就是non-const 到const的转换。
为了展示的方便,会忽略掉一些代码
template<typename T>
bool func(const T t1, const T t2)
{
return less<T>()(t1,t2);
}
...
int a = 10;
const int b = 20;
func<int>(a,b);
这里的int转为cosnt int是允许的,因为这是按值传递,并且non-const 转为 cosnt也不会带来什么坏处。因此,编译器会执行这样的转换。
我们不妨修改一下这个函数
bool func(const T & t1, const T & t2);
bool func(T & t1, T & t2);
上述第一个声明会正常调用——虽然是引用,和上面的是同样的道理。
而第二个却不会正常调用——因为b是一个cosnt 将要转换为non-const,我中转换是不安全的,所以编译不允许这样的转换。
假设我们这样调用两个函数
func<int>(10,20);
同样的,对于第一个是允许的——虽然讲右值绑定到左值引用上,但是我们用const修饰形参,保证其不会改变,所以编译器同意这样的转换。
而对于第二个,编译器则不允许这样的转换,因为我们的形参是non-const的,不能保证不修改形参的值,形参正好又是一个引用,这样可以修改实参的值——恰好实参是一个右值——是不允许被修改的,所以编译器不允许这样的转换。
基于上面的转换规则,我们可以知道,如果函数形参不是引用类型,则可以对数组或者函数类型的实参应用正常的指针转换。
上面的是C++ Primer的原文,实际上笔者在学习的过程中,发现了其错误。
先来看一下代码
template <typename T> bool func(const T & t1, const T & t2);
...
int a[10];
int b[10];
func(a,b);
这样是可以的。那这样呢?
template <typename T> T func(const T & t1, const T & t2);
template<typename T1, typename T2, typename T3>
T1 sum(T2 t2, T3 t3)
{
return t2 + t3;
}
...
sum<long long>(10,200); //or sum<long long, int, int>();
就不可以了,这个声明对应着C++ Primer的声明。
为什么一样的形参列表只有返回值不同编译器就会发出警告,这是为什么。
其实我们到目前为止的讨论,都适用于普通函数,模板的本质其实也是模板为我们生成对应的版本,为了解开上面的疑惑,我们可以先来复习以下引用的知识。
int *& ref_apple_point = &apple; //error
int * const & ref_apple_point_const = &apple; //ok
根据这两行代码我们可以得到一些启示。
因为数组名是一个常量,const T & t1这样的形参是可以接受的。但对于返回值来说,可就麻烦了。返回值为内置数据类型的模板函数,对于这个问题,这没有什么好说的。返回类型为T的模板函数,他返回的是一个什么具体类型呢?首先T被u推断为* const,那么返回类型也应该是 *const
在这里我们先留下一个悬念,当我们理解和编译器是如何推断T是何种类型的时候,这个问题可能就会迎刃而解。
返回值类型推断
在编译器遇见函数列表之前,所有的形参都是不存在的,那么我们需要使用这样的尾置返回类型。
auto func(It & _beg, It & _end) -> decltype(*_beg)
{
//...
auto sum = *_beg;
sum = 0;
for_each(_beg,_end,[&sum](const int & val){ sum+= val;});
//...
return sum;
}
这样的代码还是有一些问题的,如果我们要返回一个拷贝而不是引用呢?要用到一个类型转换模板工具。
remove_reference<> 移除引用——关于其他的类型转换,不再本文章讨论范围内读者可自行查阅。
这个模板类有一个名为type的public成员,能够获得相应的类型。所以我们可以这样写
template <typename It>
auto func(It & _beg, It & _end) -> typename remove_reference<decltype(*_beg)>::type //don't forget typename
{
//...
auto sum = *_beg;
sum = 0;
for_each(_beg,_end,[&sum](const int & val){ sum+= val;});
//...
return sum;
}
在某些情况下我们可以指定返回u类型,例如
template<typename T1, typename T2, typename T3>
T1 sum(T2 t2, T3 t3)
{
return t2 + t3;
}
sum<long long>(10,200); //or sum<long long, int, int>();
显示模板参数按从左到右的顺序一次匹配。
兼容类型的模板问题
有这样的代码
template<typename T>
T sum(T t1, T t2)
{
return t1 + t2;
}
sum(10,3.14);
虽然int和double兼容,但是只有一个类型参数,编译器傻了,T为int?精度会丢失,肯定是不可行的,T为double?貌似也不行,这样会导致数据溢出。无奈我们只好这样了。
template<typename T1, typename T2>
??? sum(T1 t1, T2 t2)
{
return t1 + t2;
}
至于返回类型,全交给程序员来规定,或者用尾部返回类型。
函数指针实参推断
有趣的是,虽然在未实例化之前,编译器没有生成具体的代码,但我们仍然可以进行函数指针绑定的操作。
template <typename T> int compare(const T & t1, const T & t2) { }
int (*pf_int)(const int &,const int &) = compare;
同样的我们也可以将模板函数作为回调函数进行传参,但此时可能会产生二义性,所以注意显示的写出模板参数。
当参数是一个函数模板实例的地址时,程序上下文必须满足:对于每个模板参数,能唯一确定其类型的值。
模板实参推断
这里是重中之重!!!重中之重!!!
很多的模板问题都与此有关。
关于const和&的问题,我们上面已经讲过了。这里再进行进一步的说明。
左值引用
template <typename T> void func1(T &) { }
template <typename T> void func2(const T &) { }
void aa()
{
int a = 10;
const int b = 20;
func1(a); //T is int
func1(b); //T is const int
func2(a); //T is int
func2(b); //T is int
func2(10); //T is int
}
还是比较有意思的,看func2(b)的调用,虽然我们将const int类型传入进去,但是编译器为我们推导的还是int,原因应该和参数类型有关,如果编译器为我们推导的是const int ,那么const const int是不合法的,所以只好为我们推倒为int,即使我们调用时候的类型是const int。
右值引用
template <typename T> void func(T &&);
func(10); //T is int
func(b); //b is a left_val T is ???
我们可以根据引用折叠可以推断出类型。
引用折叠和万能引用
众所周知在非模板函数中可以使用const & 来接受任意类型参数,在模板中,也有类似这样的万能引用,就是&&。知道了这样的原因是有着引用折叠得的存在。
先说结论:在传递参数的过程中,无论多么复杂的引用传参,最后都会被折叠为& 或者 &&.
如果我们间接创建了一个引用的引用,则这些引用形成折叠。除了右值引用的右值引用会被折叠为一个右值引用,剩下全部折叠为一个左值引用。即
T& &, T& &&, T&& &都会折叠为T&
T&& &&会被折叠为&&
这就意味着我们 可以解释上面的问题。
当我们将一个左值传递给一个右值引时候,编译器推断T的类型为&。注意是T的类型为左值引用,不是整个形参是T &。
所以
func(b); //b is a left_val T is int&
上述的两个规则导致了
如果一个函数参数是一个指向模板类型参数的右值引用,则他可以被绑定到一个左值。
如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数将被参数将被实例化为一个普通左值引用参数。
这两个规则又暗示了我们——我们可以将任意类型的实参传递给参数为右值引用的函数。
当代码中涉及的类型可能是非引用类型,也可能是引用类型的时候,编写正确的代码就变得异常困难(虽然remove_reference这样的转换类型对我们可能有所帮助)。
PS:由于这里的知识是在是很乱,笔者在写这里的时候也实在无能为力,所以大量了引用C++ Primer的原文。但是有一点可以保证——笔者在这里写的demo虽然没有什么实际意义仅用于演示——但是也能说明一些问题。
如果读者对模板的细节想以探究经,可以翻越C++ Primer——中文第五版P508-P610。
如果想巩固这里的语法,可以作相应的配套习题。
std::move
折磨的篇章终于过去了,让我们用好奇心来看一看std::move这个工具。
短小精悍的std::move定义
如下
template <typename T>
typename remove_reference<T>::type && move(T && t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
std::move的解析
因为move可以接受任意对象,所以应当是一个模板类。
既然我们要保证返回一个右值,那我们应当明确的得到一个非左右值引用类型——即普通类型。
那么就可以先移除引用再加上右值引用——这样保证了返回一个右值引用对应了
typename remove_reference<T>::type &&
既然接受任意一个对象,那美可以用&&来接受实参,对应
move(T && t)
我们只需要返回一个右值即可,所以只有一个return语句。
我们回想以下为什么要使用std::move——获得一个右值进行移动构造?又或者是仅仅需要一个右值?不管出于什么原因,最终的目的就是为了优化程序,所以通过形参创建一个额外的右值并返回这样是不可取的,是脱裤子放屁,所以我们要使用这条语句
static_cast<typename remove_reference<T>::type&&>(t);
通常情况下,static——cast用于合法的类型转换,但是又一种情况例外,虽然一个左值不能隐式转换为右值,但是可以使用static_cat将其显示的转换为右值——前提我们先移除对象身上的引用效果。
模板和重载
模板也是可以被重载的,只要没有二义性。像在C++库中,存在着大量的模板重载技术,或者是可变模板参数中,也存在着模板的重载。
对于实例化的选择,遵循以下的规则。
1.对于一个调用,其候选函数是所有可行的实例化
2.可行函数按类型转换来排序。当然,可用于函数模板调用和的类型转换是非常有限的。
3.和普通函数一样,如果恰又一个函数比任何其他函数都更好的匹配,则选择此函数。
4.如果多个函数提供了同样好的匹配
1)优先选择非模板函数
2)没有非模板函数我选择更加特例化的模板
3)否则有二意性
正确的定义一组重载的函数模板需要对类型键的关系以及模板幻术允许的优先的实参类型转换有着深刻的理解。
注意:虽然非模板函数的优先级很高——但那也是没有对应模板匹配的情况下,所以,在重载模板的时候仔细观察和思考。
所以我们为了适配字符串的比较,可以写出这样的代码
template<size_t N, size_t M>
int compare(const char str1[N], const char str2[M])
{
return strcmp(str1,str2);
}
//或者
int compare(const char * const str1, const char * const str2)
{
return strcmp(str1,str2);
}
根据上面的匹配规则,我们还可以递归的调用模板类自己实现某些功能
template<typename T> string debug_rep(const T & t)
{
ostringstream ret;
ret << t ;
return ret.str();
}
template<typename T> string debug_rep(const T * p)
{
ostringstream ret;
ret << "pointer :" << p;
if(p != nullptr)
ret << " " << debug_rep(*p);
else
ret << " nullptr";
return ret.str();
}
//适配C风格字符串
string debug_reo(char * p)
{
return debug_rep(string(p)); //这是一个右值,不能获取其地址
}
string debug_rep(const char * p)
{
return debug_rep(string(p));
}
int main()
{
string s("hello");
string* ps = &s;
cout << debug_rep(ps) << endl << debug_rep(s);
return 0;
}
运行结果
模板函数匹配的特殊性
难道没有发现一个异常的地方吗?对于指针版本的调用,可以这样实例化两个函数
string debug_rep(const string* & t);
string debug_rep(string * p);
对于普通函数,这是无疑的二义性,但是模板会选择特例化高的,原因是const T&可以实例化任何类型,而const T * p只能实例化指针类型——特例化程度更高。所以会调用后者。
这就说明了:不要将普通函数的匹配机制应用于模板函数匹配机制——虽然两者很像,但是还是有某些地方是不一样的。
注意重载模板声明顺序
由于模板的特性,使得其可以递归的调用自己的不同版本,但是注意要调用的版本一定要事先声明或者定义,否则可能出现函数不匹配的情况
我们把适配char * 接口的字符串的函数放到最前面,我们发现编译器会右值河阳的错误。
No matching function for call to 'debug_rep'
调用“debug_rep”没有匹配的函数
或者我们将两个debug_rep的模板版本调换以下顺序。
Call to function 'debug_rep' that is neither visible in the template definition nor found by argument-dependent lookup
调用函数“debug_rep”,该函数在模板定义中既不可见,也不通过参数相关查找找到
模板特例化
我们编写的模板,不可能保证对于所有的类型都能适用——compare函数就是很经典的例子,对于两个指针类型,仅仅是毫无意义的比较。这时候我们用到模板特例化的技术可以很好的解决这样的问题。
由于less的底层是使用<来比较的,所以less并没有适配字符指针。那么,我们可以编写这样的模板特例化。
template <> //表示一个模板特例化——语法规定
int compare(const char * const & str1, const char * const & str2) //具体的类型
{
return strcmp(str1,str2);
}
可以看出,模板特例化的尖括号中没有任何说明,所以模板特例化要对所有模板那参数都进行特例化。
注意,上面的特例化只能处理字符指针,不能处理数组或者字符串面量——这和函数匹配机制有关。这个特例化仅仅接受char*以及其const版本,虽然字符数组的名字就是他的地址,但是在模板中会被解释为一个字符数组的引用,更加的精准匹配。如果想要支持字符面量(本质上是字符数组)和字符数组,请写一个重载的模板函数——见模板重载。
我们可以使用调试观察是如何推断实参类型的
推断为一个数组的引用——这显然比将数组转换为指针再进行匹配更加精确。
特例化和重载 的区别
特例化就是一个特殊的实例化——模板的实例化,所以,特例化仅仅是模板的一个实例化,不会影响函数匹配。
并且,模板特例化一定要保证模板的之前的声明或者定义。如果不这样做——编译器不会报错,但是会有一些令人匪夷所思的地方。模板会由编译器实例化,而不是调用自己特例化版本——这种错误往往很难查找。所以,记住一个规则:特例化一个模板,一定要保证其在原模板的定义域中。
类模板特例化
这里引用《C++ Primer》的例子——对其做一些解析。
hash容器是能够进行十分快速的查找容器,像hash_map, hash_set等。他们的底层使用什么来映射哈希值呢? hash模板类。hash - C++ Reference (cplusplus.com)
那么对于我们自定义的类型来说,没有其对应算法,为了能够使用我们的自定义类型,我们可以定义一个其特例化版本。
一个hash的特例化必须包括
一个重载的调用运算符,接受一个容器关键字类型的对象,返回一个size_t——用于映射对象的哈希值。
两个类型成员,result_type, argument_type,分别调用运算符返回类型和参数类型。
默认构造函数和拷贝赋值运算符。
于是我们可以写出如下的代码。
class Book
{
friend class std::hash<Book>; //hash使用了私有成员,所以将其声明为友元
private:
int book_id;
string book_name;
public:
Book() = default;
Book(const int a, const string & _name) : book_id(a), book_name(_name) { }
bool operator==(const Book & b) const
{
return book_id == b.book_id;
}
};
我们首先定义一个Book类,然后提供其==运算符确保hash模板能够自持我们的自定义类型。
随后我们在std命名空间中特例化一个和hash,
namespace std
{
template <>
struct hash<Book>
{
//必须提供的成员
typedef size_t result_type;
typedef Book argument_type;
size_t operator()(const Book & b) const;
};
size_t
hash<Book>::operator()(const Book &b) const {
//自定义如何组织hash_val
return hash<string>()(b.book_name) ^ hash<int>()(b.book_id);
}
}
之后,我们就可以使用unordered_set/map,来操纵我们的自定义类型了。
为了能够让自定义数据类型的特例化能够被正常使用,应该将其放在类声明对应的头文件中,或者用别的头文件将其包含进来。
部分模板特例化
我们可以指定一部分而非所有的模板参数,或者是参数的一部分而非全部特性。一个模板的部分特例化本身是一个模板,使用它时用户还必须为哪些在特例化版本呢中未指定的模板参数提供实参。
部分特例化一部分模板参数特例化,没有特例化的部分额外的提供实参。
标准库的remove_reference 就是使用一系列的特例化完成其功能的,我们将其转到定义。
这里部分特例化的时参数的引用性质。
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
具体的语法:
在正常的模板声明之后,在类的后面使用尖括号<>放入要特例化的实参,这些实参于原始模板中的参数按照位置对应。也就是对应着上面源码中的
struct remove_reference<_Tp&&>
注意:模板部分特例化并不是对于某个单单的模板参数特例化,也可能是模板参数属性的特例化,这点对于理解类模板部分特例化十分重要
注意:我们只能部分特例化类模板,而不能部分特例化模板函数。
例如我们定义一个泛用的模板类和实例化一个其专门用来处理指针的类
template <typename T>
class A;
template <typename T>
class A<T *>;
特例化成员
我们可以只特例化特定的成员函数而不是特例化整个模板类。
template <typename T>
class A
{
private:
T val;
public:
A(const T & t) : val(t) { }
void func();
};
template<>
void A<int>::func()
{ }
那我们这样得到的就是对于A<int>的实例化下的一个特例化func——这个func只在int的实例化版本生效。也就是说,实例化int版本的A其对应的func是我们特例化的这个版本,而其他成员还是正常的实例化。
可变参数模板
声明:关于模板的递归调用,都有一个基线函数,只不过是没有写出。
关于可变参数模板,一部分引用我之前的文章,再在这里做一些补充
可变模板参数 variadic template
包 packet
模板参数包 template parameter packet
函数参数包 function paremeter packet
详情见这篇文章的可变模板参数。
补充:
sizeof...运算符
能够获得包中参数的个数
template<typename T, typename... Args>
void var_fun(const T & t, const Args&... args)
{
//cout << t;
cout << "element numbers of packs is " << sizeof...(Args);
//var_fun(args...);
}
包拓展
拓展 packs expand
包拓展简单的来说将他分解为其构成的元素,如果说将参数变为包的过成类比为压缩未见,那么包拓展就是解压文件,但包拓展不仅仅是包展开。
当拓展一个包时,我们还要提供用于,每个拓展元素的模式。拓展一个包就是将它费解为构成的原书,对每个元素应用模式,获得拓展后的列表。我们通过在模式右边防一个省略号...来触发拓展操作 。
什么是模式?
在实际生活中,当我们说以一种模式打开某个东西,或者是什么模式打开时。指定的是固有的模式,比如说性能模式,均衡模式等。而抱拓展的模式更像是对于每个元素都调用一次相应的函数,包拓展需要我们自定义模式——其实就是一个函数,返回值为包中的一个元素应用模式后的结果,所有这样的结果组合在一起,也就是包以这个模式(函数)展开。
看一下标准库的配置器中是如何使用的展开
noexcept(noexcept(::new((void *)__p)
_Up(std::forward<_Args>(__args)...)))
这是一个函数异常声明的部分,当用一个包构造一个元素的时候不会抛出异常,仅当,使用转发模式对参数包进行展开的时候不抛出异常。
var_func(args...); //默认的包展开
//注释部分的...不为关键字,和C++语法没有任何关系
//相当于这样{ele1, ele2, ele3, ... ,elen}
var_fun(mul(2,args)...); //带有模式的包展开
//第二种展开模式相当于这样
//{ mul(2,ele0),mul(2,ele1),mul(2,ele2), ... mul(2,elen) }
具体实验
template <typename T>
void print(const T & t)
{
cout << t << endl;
}
template <typename T, typename... Args>
void print(const T &t ,const Args... args)
{
cout << t << endl;
print(args...);
}
template <typename T>
int up(T & t)
{
t *= 2;
return t;
}
template <typename... Args>
void func(Args&&... args)
{
print(up(args)...);
}
int main()
{
func(1,2,3,4,5);
return 0;
}
运行结果
可变模板参数的具体作用
可变模板参数可以说是一个核弹,比如tuple就是使用其实现的,模板类tuple以私有继承的方式继承它自己并结合模板部分特例化。如下
template<typename T, typename ... Args>
class tuple<T,Args...> : private tuple<Args...>
{
//something
};
还是很奇妙的,具体详情请观看侯捷老师的视频——bilibili :
模板技巧
模板的功能还是很强大的,我们有必要学习一些模板技巧。
转发
什么是转发?
某些函数需要将其一个或多个实参连同类型不变的传递给其他函数。这个过程就叫转发。
很形象,一个函数把数据原封不动的传递给另一个函数,就是转发。
什么时候会用到转发呢?比如说我们有这样的一个函数,在容器尾部直接使用我们穿进来的参数构造一个元素,这个时候使用转发就是很有必要的。如果我们不适用转发技术,可能会造成变量的复制,也许有的时候这个函数能正常使用,但是有的时候我们就需要引用来做事,所以这样做留下的错误的隐患。
假设有func1(int &,args) fun2 work(args,int&);
我们需要传进func1一个整形,经过func2的中间媒介,传入work,并在work中改变那个变量。
读者可以试一下func2中使用什么样的参数,经过怎样的变换可以对原来的参数的性质原封不动的传递给work。这是比较简单的情况了。STL的部分函数实现会有恐怖的调用层次,如果不使用转发技术后果可想而知。
使用std::forward
要说转发一定离不开std::forward
forward返回实参类型的右值引用。它和move很像,但前者是返回给定类型的右值引用,如果给定的类型是左值引用也返回其右值引用——左值引用,并且其必须显式的指定模板参数;而move无论模板参数类型是什么都返回一个右值引用(只能是右值引用),因为前面已经看到了move的实现方法。
于是我们可以定义下面的转发函数
template <typename F, typename T1, typename T2>
void fun(F f, T1 && t1, T2 && t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
使用右值引用作为模板参数——确保接受任意对象,并保证其能保持原来的性质不变(见引用折叠)。在发送参数的过程中获得对应类型的右值——确保其传递给函数的参数的性质不变(见引用折叠)。
更简单的来说,上述的写法是对于所有类型的对象,无论进行何种参数传递,其参数的性质都不会改变的通用情况。
转发参数包
根据上面转发的关键字,我们可以知道,在进行转发的时候应该以何种模式进行包展开。
(Args&& ... args) //以右值引用展开参数包
std::forward<Args>(args)... //将包中的每一个元素应用于forward
所以我们可以这样做
template<typename ... Args>
void buffer_fun(Args &&... args)
{
work(std::forward<Args>(args)...);
}
make_shared的工作原理
std::make_shred就是基于转发参数包实现的。
让我们先来回忆make_shared的其中一个使用方法。
make_shared<T> name (args);
很明显的可以推断其应该使用用可变模板参数,我们转到其定义
template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp>
make_shared(_Args&&... __args)
{
typedef typename std::remove_cv<_Tp>::type _Tp_nc;
return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
std::forward<_Args>(__args)...);
}
可以看见,其使用部分特例化和可变参数模板,将包转发给了std::allocate_shared进行空间分配,我们进一步的转到std::allocate_shared中可以看见其有进一步的将其转发给了其他的模板
template<typename _Tp, typename _Alloc, typename... _Args>
inline shared_ptr<_Tp>
allocate_shared(const _Alloc& __a, _Args&&... __args)
{
static_assert(!is_array<_Tp>::value, "make_shared<T[]> not supported");
return shared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a},
std::forward<_Args>(__args)...);
}
至此,C++的模板的基础知识点大抵应该是都讲完了,如果日后有一些杂项补充的话会更在下面。
后来的话: 这篇文章有一些地方讲的还是比较不清晰的,现在已经修正了一部分,并且增添了对一部分知识点的代码,日后也会慢慢修改。如果你发现本篇文章的错误或者对本篇文章有什么建议可以评论区留言或者私信笔者。
——2022.6.1