【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怎么进行漏洞扫描?如何进行漏洞扫描?接下来就让小编带大…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...
【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...
基于IDIG-GAN的小样本电机轴承故障诊断
目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) 梯度归一化(Gradient Normalization) (2) 判别器梯度间隙正则化(Discriminator Gradient Gap Regularization) (3) 自注意力机制(Self-Attention) 3. 完整损失函数 二…...
通过 Ansible 在 Windows 2022 上安装 IIS Web 服务器
拓扑结构 这是一个用于通过 Ansible 部署 IIS Web 服务器的实验室拓扑。 前提条件: 在被管理的节点上安装WinRm 准备一张自签名的证书 开放防火墙入站tcp 5985 5986端口 准备自签名证书 PS C:\Users\azureuser> $cert New-SelfSignedCertificate -DnsName &…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...
C++_哈希表
本篇文章是对C学习的哈希表部分的学习分享 相信一定会对你有所帮助~ 那咱们废话不多说,直接开始吧! 一、基础概念 1. 哈希核心思想: 哈希函数的作用:通过此函数建立一个Key与存储位置之间的映射关系。理想目标:实现…...
raid存储技术
1. 存储技术概念 数据存储架构是对数据存储方式、存储设备及相关组件的组织和规划,涵盖存储系统的布局、数据存储策略等,它明确数据如何存储、管理与访问,为数据的安全、高效使用提供支撑。 由计算机中一组存储设备、控制部件和管理信息调度的…...
【Zephyr 系列 16】构建 BLE + LoRa 协同通信系统:网关转发与混合调度实战
🧠关键词:Zephyr、BLE、LoRa、混合通信、事件驱动、网关中继、低功耗调度 📌面向读者:希望将 BLE 和 LoRa 结合应用于资产追踪、环境监测、远程数据采集等场景的开发者 📊篇幅预计:5300+ 字 🧭 背景与需求 在许多 IoT 项目中,单一通信方式往往难以兼顾近场数据采集…...


而对于C++11来说,C++11使这个特性实践的更加广泛。