C++可变参数模板解析

前言

考虑一个经典的场景:

需要编写一个函数,打印变量信息

比如:

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)...));
}
  1. Args&& 表示万能引用,可以接收左值引用,也可以接收右值引用。
  2. 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 来获取移除后的类型进行匹配。

来源:c++ 可变参数模板解析 - 掘金 (juejin.cn)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值