【C++修炼之路】30.可变参数模板包装器
每一个不曾起舞的日子都是对生命的辜负
C++11之可变参数模板&&包装器
- 前言
- 一.可变参数模板的首次登场
- 二.参数包展开
- 2.1 递归函数方式展开参数包
- 2.2 逗号表达式展开参数包
- 三.容器的emplace方法
- 四.包装器
- 4.1 什么是function
- 4.2 function包装器的作用
- 4.3 function的实际用途
- 4.4 什么是bind
- 4.5 bind的作用
前言
在学习C语言时,就有过这种可变的参数数量的函数,即我们耳熟能详的scanf和printf,因为其可以传任意数量的参数:
而对于C++11来说,C++11使这个特性实践的更加广泛。
C++11的新特性可变参数模板能够创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段,我们掌握一些基础的可变参数模板特性就够我们用了。
一.可变参数模板的首次登场
#include<iostream>
#include<vector>
using namespace std;//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包可以包含0到任意个模板参数。
template<class ...Args>
void ShowList(Args... args)
{//查看参数包中有几个参数cout << sizeof...(args) << endl;//for (size_t i = 0; i < sizeof...(args); i++)//可惜的是可变参数列表不支持[]重载//{// cout << args[i] << endl;//}
}int main()
{//想传几个就传几个,想传什么类型就传什么类型ShowList();ShowList(1);ShowList(1, 1.1);ShowList(1, 1.1, string("xxxxxx"));return 0;
}
既然可变参数列表不支持[]访问,那么如何能够进行访问呢?
二.参数包展开
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
2.1 递归函数方式展开参数包
void ShowList()
{cout << endl;
}//args参数包可以接收0-N个参数包,而下面这个由于存在T就接收1-N个参数包
template<class T, class ...Args>
void ShowList(T val, Args... args)
{cout << val << " ";ShowList(args...);
}int main()
{ShowList();ShowList(1);ShowList(1, 1.1);ShowList(1, 1.1, string("xxxxxx"));return 0;
}
通过函数重载+递归的方式就可以完成,因为从模板函数中可以看出每次递归的参数都会减少1个,当减少到0个的时候,就会因为不满足模板函数的参数范围要求,就会去调用上面参数为0的函数,此时就停止递归了。
2.2 逗号表达式展开参数包
template<class T>
void PrintArg(T t)
{cout << t << " ";
}//展开函数
template<class ...Args>
void ShowList(Args... args)
{//逗号表达式:结果为后面的值,通过可变参数列表展开并推演个数,进行实例化调用上面的函数。int arr[] = { (PrintArg(args), 0)... };cout << endl;
}int main()
{//ShowList();ShowList(1);ShowList(1, 1.1);ShowList(1, 1.1, string("xxxxxx"));return 0;
}
通过逗号表达式的方式,可变参数列表能够推演数组的大小并将参数进行实例化从而调用PrintArg(T t)
,需要注意的是,这种方式不能传0个参数,即上面注释的ShowList()
,因为不能分配常量大小为 0 的数组。
当然,也可以优化一下:
template<class T>
int PrintArg(T t)
{cout << t << " ";return 0;
}//展开函数
template<class ...Args>
void ShowList(Args... args)
{//逗号表达式:结果为后面的值,通过可变参数列表展开并推演个数,进行实例化调用上面的函数。int arr[] = { PrintArg(args)... };cout << endl;
}int main()
{//ShowList();ShowList(1);ShowList(1, 1.1);ShowList(1, 1.1, string("xxxxxx"));return 0;
}
将逗号表达式换成PrintArg带有返回值的方式,因为数组里面初始化必须有值,除了逗号表达式就是这种方式,相比前者,这种更直观。
三.容器的emplace方法
对于各种容器的emplace、emplace_back方法,由于是c++11新出的方法,参数无论是右值还是左值,都存在一个可变参数列表为函数的重载函数,其功能与push、push_back是一样的。
template <class... Args>
void emplace_back (Args&&... args);
下面就来演示一下:
int main()
{list<int> list1;list1.push_back(1);list1.emplace_back(2);list1.emplace_back();//下面的参数过多底层就不识别了//list1.emplace_back(3, 4);//list1.emplace_back(3, 4, 5);for (auto& e : list1){cout << e << endl;}return 0;
}
底层仅仅支持0到1个参数,接下来看看下面:
int main()
{list<pair<int, char>> mylist;mylist.push_back(make_pair(1, 'a'));//构造+拷贝构造//mylist.push_back(1, 'a'); //push_back不支持//emplace_back支持mylist.emplace_back(1, 'a');//直接构造for (auto& e : mylist){cout << e.first << " : " << e.second << endl;}return 0;
}
所以,emplace_back有时比push_back更快,就是因为其底层存在着参数包,不用进行拷贝构造。当然,emplace_back也可以直接传对象。
这就可以看出,为什么通过emplace_back()更快,如果没有实现移动构造,那么这两个的差别就会非常的大。(尤其是对于一些内容较多的类:如string等)
emlplace就是少拷贝一次,直接构造,没有参数上的拷贝过程,因此如果对于没有实现移动构造的深拷贝的类,减少的就是一次深拷贝,性能就会提升很多。
前三个标题都是介绍的可变参数模板,下面是新的主题:包装器。
四.包装器
c语言的函数指针,C++的仿函数/仿函数对象、lambda都是之前学过的,今天新增一个包装器:function
4.1 什么是function
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。(实际上是类模板)
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
4.2 function包装器的作用
对于如下代码:
#include<iostream>
using namespace std;ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lambda表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}
上述方式,导致效率低下的原因是该模板被不同的类实例化了三次,如何证明?
count作为static类型,每一次的地址都不同,所以可以看出,实例化了三次。为了防止这种方式造成的效率低下,使其只实例化一份,让这个地方统一一下,这就利用到了function:
function包装器的作用: 对各种可调用对象进行类型统一
#include<functional>
using namespace std;int f(int a, int b)
{return a + b;
}struct Functor
{
public:int operator() (int a, int b){return a + b;}
};class Plus
{
public:static int plusi(int a, int b){return a + b;}int plusd(int a, int b){return a + b;}
};
int main()
{//不是特化,而是和vector一样,是类的实例化,但传统的都是一个一个参数,function不一样//1.封装函数指针:两种初始化方式function<int(int, int)> f1;f1 = f;function<int(int, int)> f2(f);//2.封装仿函数对象f1 = Functor();//function<int(int, int)> f3(Functor()); 有点怪,vs和g++(版本4.8)都识别不了,可能是把这个看成函数指针了//Functor ft;//function<int(int, int)> f3(ft);//这种就可以//函数对象function<int(int, int)> f3 = Functor();cout << f1(1, 2) << endl;cout << f3(1, 2) << endl;//3.封装lambdafunction<int(int, int)> f4 = [](const int a, const int b) { return a + b; };cout << f4(1, 3) << endl;//4.封装成员函数: 函数指针function<int(int, int)> f5 = &Plus::plusi; //类静态成员函数指针--static修饰的&可加可不加cout << f5(1, 2) << endl;// -----------------------------------------------------------------------------------以上都是同一个类型function<int(Plus, int, int)> f6 = &Plus::plusd;//需要加上类名cout << f6(Plus(), 1, 2) << endl;//因为this指针不能显式调用,所以需要直接加Plus()return 0;
}
对于最下面的f6与上面的不是同一个类型,但是可以通过特殊处理让其与之前的变成一个类型,即:
Plus plus;
function<int(int, int)> f6 = [&plus](int x, int y)->int {return plus.plusd(x, y); };
cout << f6(1, 2) << endl;//因为this指针不能显式调用,所以需要直接加Plus()
因此,最开始实例化三次的代码我们也可以用包装器让其置为同一类型:
#include<functional>
int main()
{// 函数名cout << useF(function<int(double)>(f), 11.11) << endl;// 函数对象Functor ft;cout << useF(function<int(double)>(ft), 11.11) << endl;// lamber表达式cout << useF(function<int(double)>([](double d)->double { return d / 4; }), 11.11) << endl;return 0;
}
通过这样的包装器处理就不会因为类型不同导致实例化多次造成效率低下了。
4.3 function的实际用途
可以进行符号和函数之间的响应
就比如Leetcode150.逆波兰表达式求值,就可以通过c++11的方式进行编写:
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;map<string, function<int(int, int)>> opFuncMap = {{"+", [](int x, int y)->int{return x + y;}},{"-", [](int x, int y)->int{return x - y;}},{"*", [](int x, int y)->int{return x * y;}},{"/", [](int x, int y)->int{return x / y;}}};for(auto& str : tokens){if(opFuncMap.count(str) == 0){st.push(stoi(str));}else{int right = st.top();st.pop();int left = st.top();st.pop();st.push(opFuncMap[str](left, right));}}return st.top();}
};
通过以上的方式,即便又增加了一种新的操作,只需要在opFuncMap里面继续添加就好了。
4.4 什么是bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
4.5 bind的作用
bind绑定可以减少使用时参数传递的个数。
#include<iostream>
#include<functional>
using namespace std;
int Plus(int a, int b)
{return a + b;
}int SubFunc(int a, int b)
{return a - b;
}
class Sub
{
public:int sub(int a, int b){return a - b * x;}
private:int x = 20;
};int main()
{//表示绑定函数plus,参数分别由调用 func1 的第一、二个参数指定,占位对象function<int(int, int)> func1 = bind(Plus, placeholders::_1, placeholders::_2);cout << func1(1, 2) << endl;function<int(int, int)> func2 = bind(SubFunc, placeholders::_1, placeholders::_2);cout << func2(1, 2) << endl; // -1//调整参数的顺序function<int(int, int)> func3 = bind(SubFunc, placeholders::_2, placeholders::_1);cout << func3(1, 2) << endl; // 1function<bool(int, int)> gt = std::bind(less<int>(), placeholders::_2, placeholders::_1);cout << gt(1, 2) << endl;//固定绑定参数:减少参数的传递function <int(Sub, int, int)> func4 = &Sub::sub;cout << func4(Sub(), 10, 20) << endl;//-390//绑定之后相当于减少了参数的个数,_1和_2代表参数传递的顺序function <int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);cout << func5(10, 20) << endl;//-390return 0;
}
相关文章:

【C++修炼之路】30.可变参数模板包装器
每一个不曾起舞的日子都是对生命的辜负 C11之可变参数模板&&包装器 前言一.可变参数模板的首次登场二.参数包展开2.1 递归函数方式展开参数包2.2 逗号表达式展开参数包 三.容器的emplace方法四.包装器4.1 什么是function4.2 function包装器的作用4.3 function的实际用途…...

Linux防火墙之firewalld基础
一、firewalld概述 firewalld防火墙是Centos7系统默认的防火墙管理工具,取代了之前的iptables防火墙,也是工作在网络层,属于包过滤防火墙。 firewalld和iptables都是用来管理防火墙的工具(属于用户态)来定义防火墙的…...

GitLab CI/CD
CI/CD 简介 CI/CD 简单来说就是可以自动化编译、测试、打包我们的代码。 GitLab CICD的使用 首先需要安装gitlab-runner。 在GitLab 中,runners 是运行 CI/CD 作业的代理。我们的对代码的作业都是在runner上去执行的。我们可以在本地、服务器、等任意一个联网设…...

PHP复习资料(未完待续)
(未完待续,请持续关注此板块) 【计科三四】雪课堂PHP期末模拟题:https://ks.wjx.top/vm/tUAmjxq.aspx# 【计科一二】PHP第一章练习题 https://ks.wjx.top/vm/QnjHad4.aspx# 【计科一二】PHP第二章练习题 https://ks.wjx.top/vm/h2…...
【python】pytorch包(第二章)API使用与介绍
1> nn.Module (用于构建模型的底层逻辑) 介绍 nn.Module 是 torch.nn 中的一个类,是pytorch中自定义网络的基类 __init__需要调用super方法,继承父类属性和方法forward方法必须实现,用来定义网络的向前计算的过程…...
Linux驱动基础(SR501人体感应模块)
文章目录 前言一、SR501模块介绍二、设备树编写三、驱动编写1.确定主设备号2.编写file_operations结构体3.注册file_operations结构体4.出口函数编写5.probe函数和remove函数编写6.中断编写7.测试程序编写8.全部驱动程序 总结 前言 本篇文章将给大家介绍一下SR501驱动程序的编…...

Android Studio Flamingo (火烈鸟) 升级踩坑记录
由于想要验证Compose最新的debug特性,而我目前使用的版本(Dolphin 小海豚)不支持,查看官网说明需要最新版本,所以不得已进行了一下Android Studio版本升级,过程中遇到一些问题,本文仅做记录。&a…...

【JAVA凝气】异常篇
哈喽~大家好呀,这篇来看看JAVA异常篇。 目录 一、前言 二、Exception 异常 1、Java 的非检查性异常 2、Java 检查性异常类 三、Error 错误 四、捕获异常 五、多重捕获块 六、throws/throw 关键字 七、自定义异常类 八、图书推荐 一、前言 异常是程序中的一…...

C++中的函数模板
目录 1. 什么是函数模板? 2. 如何定义函数模板? 3. 如何使用函数模板? 4. 函数模板与函数重载的区别是什么? 5. 函数模板与类模板有何异同点? 1. 什么是函数模板? - 函数模板是一种通用的函数描述&…...

MapReduce【Shuffle-Combiner】
概述 Conbiner在MapReduce的Shuffle阶段起作用,它负责局部数据的聚合,我们可以看到,对于大数据量,如果没有Combiner,将会在磁盘上写入多个文件等待ReduceTask来拉取,但是如果有Combiner组件,我们…...

postman接口自动化测试
Postman除了前面介绍的一些功能,还有其他一些小功能在日常接口测试或许用得上。今天,我们就来盘点一下,如下所示: 1.数据驱动 想要批量执行接口用例,我们一般会将对应的接口用例放在同一个Collection中…...

历经70+场面试,我发现了大厂面试的套路都是···
今年的金三银四刚刚过去,我又想起了我在去年春招时面试了50余家,加上暑期实习面试了20余家,加起来也面试了70余场的面试场景了。 基本把国内有名的互联网公司都面了一遍,不敢说自己的面试经验很丰富,但也是不差的。 …...

可视区域兼容性问题的思考及方法封装
今日在复习可视化尺寸获取时突发奇想,为什么要在怪异模式下使用document.body.clientWidth,在标准模式下使用document.documentElement.clientWidth?以及是否在IE8及以下的版本中其中一个获取方式将返回undefined或0。 出于该问题的思考&am…...

安全工具 | CMSeeK [指纹识别]
0x00 免责声明 本文仅限于学习讨论与技术知识的分享,不得违反当地国家的法律法规。对于传播、利用文章中提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本文作者不为此承担任何责任,一旦造成后果请自行承担…...

Android新logcat使用技巧
Android新logcat使用技巧 logcat新UI出现后,我常困惑于怎么过滤log,和以前的UI差异比较大,新UI界面结构如下: 这个新的 logcat 的问题是如何过滤信息并不是很明显。 获取应用的日志信息 要获取我们当前调试应用的日志信息&…...
使用Makefile笔记总结
文章目录 一、简单了解Makefile1.1 Makefile示例1.2 基本规则1.3 make是如何工作的1.4 使用变量1.5 make自动推导 二、变量2.1 变量的定义和引用2.2 变量的两种高级用法2.3 override 和 define 关键字2.4 环境变量与目标变量2.5 自动变量 三、Makefile规则3.1 通配符3.2 目标依…...
npm下载依赖项目跑不起来--解决方案
code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: vue-element-admin4.4.0 npm ERR! Found: webpack4.46.0 npm ERR! node_modules/webpack npm ERR! webpack“^4.23.0” from the root project npm ERR! npm ERR! Coul…...

SolVES模型生态系统服务功能社会价值评估
查看原文>>>SolVES 模型生态系统服务功能社会价值评估(基于多源环境QGIS、PostgreSQL、ArcGIS、Maxent、R语言) 目录 第一章、理论基础与研究热点 第二章、SolVES 4.0 模型运行环境配置 第三章、SolVES 4.0 模型运行 第四章、数据获取与入…...

Godot引擎 4.0 文档 - 入门介绍 - 学习新功能
本文为Google Translate英译中结果,DrGraph在此基础上加了一些校正。英文原版页面: Learning new features — Godot Engine (stable) documentation in English 学习新功能 Godot 是一个功能丰富的游戏引擎。有很多关于它的知识。本页介绍了如何使用…...

如何进行MySQL漏洞扫描
MySQL是一款广泛使用的关系型数据库管理系统,但由于其复杂的结构和功能,也存在不少安全漏洞,容易被黑客攻击。为了解决这些安全问题,进行MySQL漏洞扫描是必要的。那么MySQL怎么进行漏洞扫描?如何进行漏洞扫描?接下来就让小编带大…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
css3笔记 (1) 自用
outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size:0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格ÿ…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...

协议转换利器,profinet转ethercat网关的两大派系,各有千秋
随着工业以太网的发展,其高效、便捷、协议开放、易于冗余等诸多优点,被越来越多的工业现场所采用。西门子SIMATIC S7-1200/1500系列PLC集成有Profinet接口,具有实时性、开放性,使用TCP/IP和IT标准,符合基于工业以太网的…...
比特币:固若金汤的数字堡垒与它的四道防线
第一道防线:机密信函——无法破解的哈希加密 将每一笔比特币交易比作一封在堡垒内部传递的机密信函。 解释“哈希”(Hashing)就是一种军事级的加密术(SHA-256),能将信函内容(交易细节…...
【Redis】Redis从入门到实战:全面指南
Redis从入门到实战:全面指南 一、Redis简介 Redis(Remote Dictionary Server)是一个开源的、基于内存的键值存储系统,它可以用作数据库、缓存和消息代理。由Salvatore Sanfilippo于2009年开发,因其高性能、丰富的数据结构和广泛的语言支持而广受欢迎。 Redis核心特点:…...