前语

很快乐见到你。

咱们知道,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);

这儿有两个包拓宽:

  1. 函数的形参,在Args& 之后跟了三个点,表明将Args参数包打开,比如中打开后的函数原型是void printMsg(const int&,const float&,const double&);
  2. 第二处打开是在递归调用时,将函数参数包形参打开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)...));
}
  1. Args&& 表明通用引证,他能接纳左值引证,也能够接纳右值引证。
  2. 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来获取移除后的类型进行匹配。

总结

可变参数模板在实践的运用中,更多仍是结合完美转发来运用,完成对象的统一构造或许接口调用封装等。可变参数的存在,使得模板接口的灵敏度提升了一个档次,假如你在实践开发中遇到相似的需求,不妨运用一下,会给你带来惊喜的。

全文到此,原创不易,觉得有协助能够点赞保藏谈论转发。 有任何主意欢迎谈论区沟通指正。 如需转载请谈论区或私信沟通。
另外欢迎光临笔者的个人博客:传送门