c++可变参数模板使用示例源码解析
作者:一只修仙的猿
前言
我们知道,C++模板能力很强大,比起Java泛型这种语法糖来说,简直就是降维打击。而其中,可变参数模板,就是其中一个非常重要的特性。那什么是可变参数模板,以及为什么我们需要他?
首先我们考虑一个经典的场景:
我们需要编写一个函数,来打印变量信息。
比如:
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
也称为函数参数包
举个例子:
// T的类型是 int // Args的类型是 int、float、string 组成的模板参数包 printMsg(1,2,0.8f,"success"); // 模板会被实例化为此函数原型 void printMsg(int,int,float,string);
对于参数包,我们可以使用sizeof...
来获取该参数包中有多少个类型。如sizeof...(args); or 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++在匹配模板时,会优先匹配非可变参数模板,因此非可变参数模板则成为了递归的终止条件。这样我们就实现了一个函数,能够接受任意数量、任意类型(支持<<运算符)的参数。
特例化
递归法是最为常见的使用可变参数模板的方式。对于参数包来说,除了递归法,其次就为特例化。举个例子,还是我们上面的printMsg
函数:
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>类型的模板做了一个特例化,则在我们调用此类型的模板时,会优先匹配特例化。这也是一种处理可变模板参数的方式。
除此之外,还有很多对于可变模板参数的神奇用法,进一步提高他的灵活性。
包拓展
这里包,指的是函数参数包以及可变模板参数包。前面的例子中已经存在两个包拓展,但更多的是属于可变参数模板的语法层面,所以并没有展开说。比如上面我们提到的代码:
template<typename T,typename ...Args> void printMsg(const T& t, const Args&... args) { std::cout << t << ", "; printMsg(args...); } printMsg(1,0.8f,0.8);
这里有两个包拓展:
- 函数的形参,在
Args&
之后跟了三个点,表示将Args
参数包展开,例子中展开后的函数原型是void printMsg(const int&,const float&,const double&);
- 第二处展开是在递归调用时,将函数参数包形参展开
args...
,例子中展开后为printMsg(0.8f,0.8);
。
在涉及到函数调用、函数声明时,都需要用到上面这两个包拓展语法。但我们会发现并没有什么可以操作的空间,他更多就是一个可变模板函数的固定语法。但除此之外,包拓展可以有一个更加神奇的操作。
还是上面的例子,但是这里我们需要对打印的数据进行一轮过滤,对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判断类型的方式并不总是可靠的,这种方式会更加稳定。
此外,针对双重过滤的方式,包拓展的解决方案也会更加优雅。假如,我们在打印数据之前,需要对数据进行一次转换,之后再对转换结果进行过滤判断是否需要预警报告。那么我们的伪代码可以是如下:
template<typename T> T filterParam(const T& t) { T result = convertParam(t); if()... return result; } template<typename T> T convertParam(const T& t) {...} template<typename... Args> void printMsgPlug(const Args&... args) { printMsg(filterParam(args)...); //关键代码 }
而如果使用递归结合typeid的方式,可能就需要更多个switch进行类型匹配嵌套解决,且其结果总是不可靠的。
最后,并不是所有可变模板函数,都能使用递归去解决问题。例如我们需要一个能够构建unique_ptr的函数,他的简化版可以是这样的:
template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&... args) { return std::unique_ptr<T>(new T(fileterParam(args)...)); }
这个写法是不够完善的,但是方便我们理解。这个时候,如果我们需要对参数进行过滤,那么递归的方式,就无法在这里使用了,而必须使用包拓展。
完美转发
完美转发在可变模板中非常常见,他的作用在于保持原始的数据类型。参考我们上面的make_unique
函数,在移除fileterParam函数之后,,我们希望,传给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
表示保持参数的原始类型。因为我们知道,右值引用本身是左值,所以我们需要将其转为右值传递给构造函数。
这样,我们就能够原封不动地将数据传递给构造函数,而不修改数据类型。这部分类型属于右值与引用的范畴,这里不详细展开解析。
但是对于可变模板来说,这里有一个关键需要注意一下:通用引用的本身,是 引用类型。假如我们传递了一个int
类型进来,那么转化之后就变成了int&
。此时如果我们使用Args
类型去做模板匹配,很容易发生匹配失败的问题,会提示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来获取移除后的类型进行匹配。
总结
可变参数模板在实际的使用中,更多还是结合完美转发来使用,实现对象的统一构造或者接口调用封装等。可变参数的存在,使得模板接口的灵活度提升了一个档次,如果你在实际开发中遇到类似的需求,不妨使用一下,会给你带来惊喜的。
以上就是c++可变参数模板使用示例源码解析的详细内容,更多关于c++可变参数模板的资料请关注脚本之家其它相关文章!