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++可变参数模板的资料请关注我们其它相关文章!

(0)

相关推荐

  • C++11模板元编程-std::enable_if示例详解

    C++11中引入了std::enable_if函数,函数原型如下: template< bool B, class T = void > struct enable_if; 可能的函数实现: template<bool B, class T = void> struct enable_if {}; template<class T> struct enable_if<true, T> { typedef T type; }; 由上可知,只有当第一个模板参数为

  • 详解C++元编程之Parser Combinator

    引子 前不久在CppCon上看到一个Talk:[constexpr All the things](https://www.youtube.com/watch?v=PJwd4JLYJJY),这个演讲技术令我非常震惊,在编译期解析json字符串,进而提出了编译期构造正则表达式(编译期构建FSM),现场掌声一片,而背后依靠的是C++强大的constexpr特性,从而大大提高了编译期计算威力. 早在C++11的时候就有constexpr特性,那时候约束比较多,只能有一条return语句,能做的事情只有

  • 浅谈C++模板元编程

    所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文).模板元编程的根在模板.模板的使命很简单:为自动代码生成提供方便.提高程序员生产率的一个非常有效的方法就是"代码复用",而面向对象很重要的一个贡献就是通过内部紧耦合和外部松耦合将"思想"转化成一个一个容易复用的"概念".但是面向对象提供的工具箱里面所

  • C++模板元编程实现选择排序

    前言 模板在C++一直是比较神秘的存在. STL 和 Boost 中都有大量运用模板,但是对于普通的程序员来说,模板仅限于使用.在一般的编程中,很少会有需要自己定义模板的情况.但是作为一个有理想的程序员,模板是一个绕不过去的坎.由于C++标准的不断改进,模板的能力越来越强,使用范围也越来越广. 在C++11中,模板增加了 constexpr ,可变模板参数,回返类型后置的函数声明扩展了模板的能力:增加了外部模板加快了模板的编译速度:模板参数的缺省值,角括号和模板别名使模板的定义和使用变得更加的简

  • c++元编程模板函数重载匹配规则示例详解

    目录 前言 开始 模板函数重载匹配规则 模板匹配规则 使用 最后 前言 模板元编程,是一个听起来非常硬核的概念,会感觉这个东西非常的难,是大佬才能掌握的内容.而事实上,他也确实不简单(手动狗头),但是也并没有想象中的复杂. 我们对很多事物,都喜欢加上“元”的概念,如学习,指的是学习知识,比如学习数学.而元学习,指的是学习学习本身,去学习如何更好地学习,也就是提升学习能力.所以“元”概念,在很多时候值得就是把关注对象回到本身,比如上面的例子,把关注对象从数学等知识回到学习本身. 模板编程,指的是可

  • C++元编程语言初步入门详解

    目录 模板 泛型初步 函数模板 友元 模板参数 元编程的基本概念 可变参数模板 模板 由于模板元编程需要以面向对象为基础,所以如有疑问之处可以先补充一点C++面向对象的知识: C++面向对象这一篇就够了 泛型初步 由于C++是静态强类型语言,所以变量一经创建,则类型不得更改.如果我们希望创建一种应用广泛地复数类型,那么相应地需要基于int.float.double这些基础类型逐一创建,十分麻烦.泛型编程便是为了简化这一过程而生. 能够容纳不同数据类型作为成员的类被成为模板类,其基本方法为在类声明

  • JAVA Vector源码解析和示例代码

    第1部分 Vector介绍Vector 是矢量队列,它是JDK1.0版本添加的类.继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口.Vector 继承了AbstractList,实现了List:所以,它是一个队列,支持相关的添加.删除.修改.遍历等功能.Vector 实现了RandmoAccess接口,即提供了随机访问功能.RandmoAccess是java中用来被List实现,为List提供快速访问功能的.在Vector中,我们即可以通过

  • Project Reactor源码解析publishOn使用示例

    目录 功能分析 代码示例 prefetch delayError 源码分析 Flux#publishOn() Flux#subscribe() FluxPublishOn#subscribeOrReturn() FluxPublishOn#onSubscribe() 非融合 FluxPublishOn#onNext() FluxPublishOn#trySchedule() FluxPublishOn#run() FluxPublishOn#runAsync() FluxPublishOn#ch

  • Go Excelize API源码解析GetSheetFormatPr使用示例

    目录 一.Go-Excelize简介 二.GetSheetFormatPr 一.Go-Excelize简介 Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准. 可以使用它来读取.写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档. 支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式.图片(表).透视表.切片器等复杂组

  • Laravel源码解析之路由的使用和示例详解

    前言 我的解析文章并非深层次多领域的解析攻略.但是参考着开发文档看此类文章会让你在日常开发中更上一层楼. 废话不多说,我们开始本章的讲解. 入口 Laravel启动后,会先加载服务提供者.中间件等组件,在查找路由之前因为我们使用的是门面,所以先要查到Route的实体类. 注册 第一步当然还是通过服务提供者,因为这是laravel启动的关键,在 RouteServiceProvider 内加载路由文件. protected function mapApiRoutes() { Route::pref

  • Android 中 SwipeLayout一个展示条目底层菜单的侧滑控件源码解析

    由于项目上的需要侧滑条目展示收藏按钮,记得之前代码家有写过一个厉害的开源控件 AndroidSwipeLayout 本来准备直接拿来使用,但是看过 issue 发现现在有不少使用者反应有不少的 bug ,而且代码家现在貌似也不进行维护了.故自己实现了一个所要效果的一个控件.因为只是实现我需要的效果,所以大家也能看到,代码里有不少地方我是写死的.希望对大家有些帮助.而且暂时也不需要 AndroidSwipeLayout 大而全的功能,算是变相给自己做的项目精简代码了. 完整示例代码请看:GitHu

  • Python中getpass模块无回显输入源码解析

    本文主要讨论了python中getpass模块的相关内容,具体如下. getpass模块 昨天跟学弟吹牛b安利Python标准库官方文档的时候偶然发现了这个模块.仔细一看内容挺少的,只有两个主要api,就花了点时间阅读了一下源码,感觉挺实用的,在这安利给大家. getpass.getpass(prompt='Password: ', stream=None) 调用该函数可以在命令行窗口里面无回显输入密码.参数prompt代表提示字符串,默认是'Password: '.在Unix系统中,strea

  • Android源码解析之截屏事件流程

    今天这篇文章我们主要讲一下Android系统中的截屏事件处理流程.用过android系统手机的同学应该都知道,一般的android手机按下音量减少键和电源按键就会触发截屏事件(国内定制机做个修改的这里就不做考虑了).那么这里的截屏事件是如何触发的呢?触发之后android系统是如何实现截屏操作的呢?带着这两个问题,开始我们的源码阅读流程. 我们知道这里的截屏事件是通过我们的按键操作触发的,所以这里就需要我们从android系统的按键触发模块开始看起,由于我们在不同的App页面,操作音量减少键和电

  • .NET Core源码解析配置文件及依赖注入

    写在前面 上篇文章我给大家讲解了ASP.NET Core的概念及为什么使用它,接着带着你一步一步的配置了.NET Core的开发环境并创建了一个ASP.NET Core的mvc项目,同时又通过一个实战教你如何在页面显示一个Content的列表.不知道你有没有跟着敲下代码,千万不要做眼高手低的人哦. 这篇文章我们就会设计一些复杂的概念了,因为要对ASP.NET Core的启动及运行原理.配置文件的加载过程进行分析,依赖注入,控制反转等概念的讲解等. 俗话说,授人以鱼不如授人以渔,所以文章旨在带着大

  • 源码解析JDK 1.8 中的 Map.merge()

    Map 中ConcurrentHashMap是线程安全的,但不是所有操作都是,例如get()之后再put()就不是了,这时使用merge()确保没有更新会丢失. 因为Map.merge()意味着我们可以原子地执行插入或更新操作,它是线程安全的. 一.源码解析 default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) { Objects.requireNonNu

  • Mybatis-plus使用TableNameHandler分表详解(附完整示例源码)

    为什么要分表 Mysql是当前互联网系统中使用非常广泛的关系数据库,具有ACID的特性. 但是mysql的单表性能会受到表中数据量的限制,主要原因是B+树索引过大导致查询时索引无法全部加载到内存.读取磁盘的次数变多,而磁盘的每次读取对性能都有很大的影响. 这时一个简单可行的方案就是分表(当然土豪也可以堆硬件),将一张数据量庞大的表的数据,拆分到多个表中,这同时也减少了B+树索引的大小,减少磁盘读取次数,提高性能. 两种基础分表逻辑 说完了为什么要分表,下面聊聊业务开发中常见的两种基础的分表逻辑.

随机推荐