赞
踩
考虑一个经典的场景:
需要编写一个函数,打印变量信息
比如:
int code = 1;
string msg = "success";
printMsg(code,msg); // 输出: 1,success
而我们需要打印的参数信息是不确定的,也有可能是下面的情况:
float value = 0.8f;
printMsg(code,msg,"main"); // 输出: 1,success,main
printMsg(value,code); // 输出: 0.8,1
printMsg
的参数类型、数量都是不确定的,无论是普通模板、还是使用容器,都无法完成这个任务。而可变参数模板,可以完美完成这个任务。
可变参数模板,意为该模板的类型和数量都是不确定的,能够接收任意的参数匹配,造就了其极高的灵活度。
template<typename T,typename... Args>
void printMsg(T t, Args... args) {}
首先需要了解一个概念:模板参数包、函数参数包。
typename...
表示一个模板参数包类型,在 typename 后面跟了三个点,Args
是一个模板参数包,可以是 0 或多种类型的组合。Args...
表示将这个参数包展开,作为函数的形参,args
也称为函数参数包。
e.g.
// T的类型是 int
// Args的类型是 int、float、string 组成的模板参数包
printMsg(1,2,0.8f,"success");
// 模板会被实例化为此函数原型
void printMsg(int,int,float,string);
对于参数包,可以使用 sizeof...
来获取该参数包中有多少个类型。如 sizeof...(args)
或者 sizeof...(Args)
。
递归法利用的是类型匹配原理,将参数包中的参数,一个个分离出来。从一个实际的例子来看,假如要实现 printMsg
函数,那么是现代吗如下:
template <typename T, typename ...Args>
void printMsg(const T& t, const Args&... args) {
std::cout << t << ", ";
printMsg(args...);
}
// 调用
printMsg(1, 0.3f, "success");
当我们调用 printMsg(1,0.3f,"success")
代码时,模板函数被实例化为:
template<int,float,string>
void printMsg(const int& t, const float& arg1, const string& arg2) {
std::cout << t << ", ";
printMsg(arg1, arg2);
}
代码中再次递归调用了 printMsg()
,模板函数被实例化为:
template<float,string>
void printMsg( const float& arg1, const string& arg2) {
std::cout << t << ", ";
printMsg(arg2);
}
当我们不断递归调用 printMsg
时,参数报 Args
会被一层层解开,并将类型匹配到模板 T
上,从而将参数包 Args
中的参数逐一处理。而递归需要有终止条件。因此,我们需要在只剩下一个参数的时候将其终结:
template<typename T>
void printMsg(const T& t) {
std::cout << t << std::endl;
}
c++ 在匹配模板时,会优先匹配非可变参数模板,因此非可变参数模板则成为了递归的终止条件。这样我们就实现了一个函数,能够接受任意数量、任意类型(支持 << 运算符)的参数。
对于参数包来说,除了递归法,其次就是特例化。
template<>
void printMsg(const int& errorCode,const float& strength,const double& value) {
std::cout << "errorCode:" << errorCode << " strength:" << strength << " value:" << value << std::endl;
}
printMsg(1,0.8f,0.8);
针对 <int,float,double> 类型的模板做了一个特例化,则在我们调用此类型的模板时,会优先匹配特例化。这也是一种处理可变模板参数的方式。
PS:感觉有点脱裤子放屁。
在上述例子中,如果需要对每种类型进行过滤,比如说 int 超过 99、float 超过 0.9 就告警。可以使用递归,在每次递归中判断 T 的类型然后再根据不同的类型进行处理。但是 C++ 提供了更好的方式:
template<typename T> const T& filterParam(const T& t) { return t; } template<> const int& fileterParam(const int& t) { if (t > 99) { onWarnReport(); } return t; } template<> const float& fileterParam(const float& t) { if (float > 0.9) { onWarnReport(); } return t; } template<typename... Args> void printMsgPlug(const Args&... args) { printMsg(filterParam(args)...); //关键代码 } printMsgPlus(1,0,3f,1.8f);
关键代码在于 printMsg(filterParam(args)...);
这一行,他等价于 printMsg(filterParam(1),filterParam(0.3f) ,filterParam(1.8f));
三个小点移动到了函数调用的后面,就可以实现这样的效果。
这种方式的优点在于,他可以将过滤相关的逻辑,抽离到另外一个函数中去单独处理,利用模板的特性对数据进行统一或者单独处理。而且,使用 typeId 判断类型的方式并不总是可靠的,这种方式会更加稳定。
完美转发在可变模板中非常常见,其作用在于保持原始的数据类型。参考这个函数,需要一个能够构建 unique_ptr 的函数。在移除 filterParam()
函数之后,我们希望传给 make_unique()
函数的数据能够原封不动的传递给 T 的构造函数。那么其实现如下:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
Args&&
表示万能引用,可以接收左值引用,也可以接收右值引用。std::forward<T>()
表示保持参数的原始类型,因为我们知道,右值引用本身是一个左值,所以需要将其转为右值传递给构造函数。但是对于可变模板来说,需要注意:万能引用的本身是引用类型。假如传递一个 int
类型进来,转换之后就变成了 int&
。此时如果使用 Args
类型去匹配(因为 std::forward 需要一个模板参数,所以不得不填一个类型进去),很容易发生匹配失败的问题,会提示 int&
无法匹配到 int
类型,需要注意。要解决的问题也很简单,将其引用类型移除即可。在 c++11 中,可以使用以下代码移除所有的修饰与引用,保持基础的数据类型:
template<typename T>
using remove_cvRef = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
std::vector<decltype(remove_cvRef<T>)> v;
在匹配模板的时候,可以使用 decltype 来获取移除后的类型进行匹配。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。