【C++修炼之路】30.可变参数模板包装器

news2024/9/21 20:23:56

在这里插入图片描述
每一个不曾起舞的日子都是对生命的辜负

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,因为其可以传任意数量的参数:image-20230319122142832而对于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;
}

image-20230319123459880

既然可变参数列表不支持[]访问,那么如何能够进行访问呢?

二.参数包展开

上面的参数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;
}

image-20230319125830142

通过函数重载+递归的方式就可以完成,因为从模板函数中可以看出每次递归的参数都会减少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;
}

image-20230319131508405

通过逗号表达式的方式,可变参数列表能够推演数组的大小并将参数进行实例化从而调用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;
}

image-20230319132952697

将逗号表达式换成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;
}

image-20230319135926739

底层仅仅支持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也可以直接传对象。

image-20230319141427720


这就可以看出,为什么通过emplace_back()更快,如果没有实现移动构造,那么这两个的差别就会非常的大。(尤其是对于一些内容较多的类:如string等)

image-20230319150546794

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;
}

上述方式,导致效率低下的原因是该模板被不同的类实例化了三次,如何证明?image-20230328163710973

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.封装lambda
	function<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;
}

image-20230328172103074

对于最下面的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;
}

image-20230328194822734

通过这样的包装器处理就不会因为类型不同导致实例化多次造成效率低下了。

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();
    }
};

image-20230328190813051

通过以上的方式,即便又增加了一种新的操作,只需要在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; // 1

	function<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;//-390

	return 0;
}

image-20230329152449443

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/547345.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Redis实现短信验证码登录功能

一、概述 目前微信小程序或网站的登录方式大部分采取了微信扫码或短信验证码等方式&#xff0c;为什么短信验证码登录方式会受到互联网公司的青睐&#xff0c;因为其确实有许多好处&#xff1a; 方便快捷&#xff1a;用户无需记忆复杂的用户名和密码&#xff0c;只需通过短信…

Python共享文件 - Python快速搭建HTTP web服务实现文件共享并公网远程访问

文章目录 1. 前言2. 视频教程3. 本地文件服务器搭建3.1 python的安装和设置3.2 cpolar的安装和注册 4. 本地文件服务器的发布4.1 Cpolar云端设置4.2 Cpolar本地设置 5. 公网访问测试6. 结语 转载自内网穿透工具的文章&#xff1a;Python一行代码实现文件共享【内网穿透公网访问…

全域兴趣电商:国货品牌的新策略、新玩法

【潮汐商业评论/原创】 消费的方向标已经变了。 在消费市场的滚滚浪潮里&#xff0c;国人的“衣食住行”在全面的“国货化”&#xff0c;一个个有颜值有实力的国货品牌如雨后春笋般出现在寻常百姓家&#xff0c;如今在这片肥沃的土壤上正结出适合国人使用的果实。 01 国货二…

Openai+Coursera: ChatGPT Prompt Engineering(二)

这是我写的ChatGPT Prompt Engineerin的第二篇博客&#xff0c;如何还没看过第一篇的请先看我写的第一篇博客&#xff1a; ChatGPT Prompt Engineerin(一) Summarizing(总结/摘要&#xff09; 今天我们的重点关注按特定主题来总结文本。 设置参数 import openai openai.api_…

【备战秋招】每日一题:3月18日美团春招第二题:题面+题目思路 + C++/python/js/Go/java 带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…

深入理解递归算法

文章目录 概述单路递归 Single RecursionE01. 阶乘E02. 反向打印字符串E03. 二分查找 多路递归 Multi RecursionE01. 斐波那契数列 递归优化-记忆法递归优化-尾递归递归时间复杂度-Master theorem递归时间复杂度-展开求解 概述 定义 计算机科学中&#xff0c;递归是一种解决计…

Unity UI -- (5)增加基础按钮功能

分析分析一些常见UI 良好的UI设计会清晰地和用户沟通。用户知道他们能和屏幕上哪些东西交互&#xff0c;哪些不能。如果他们进行了交互&#xff0c;他们也要清楚地知道交互是否成功。换句话说&#xff0c;UI要提供给用户很多反馈。 我们可以来看看在Unity里或者在计算机上的任何…

一款适合国内多场景的免费ChatGPT镜像网站【建议收藏】

随着人工智能技术的不断进步&#xff0c;智能问答系统正逐渐成为我们生活中必不可少的助手。而在这个领域中&#xff0c;ChatGPT中文版-知否AI问答凭借其出色的性能和广泛的应用场景&#xff0c;成为了引领智能问答新时代的重要代表。本文将带您深入了解ChatGPT中文版-知否AI问…

LabVIEWCompactRIO 开发指南25 实施LabVIEW FPGA代码的方法

LabVIEWCompactRIO 开发指南25 实施LabVIEW FPGA代码的方法 开始开发时&#xff0c;应在LabVIEW项目的FPGA目标下创建VI&#xff0c;以便使用LabVIEW FPGA选板进行编程&#xff0c;该选板是LabVIEW选板的子集&#xff0c;包括一些LabVIEW FPGA特定函数。 应该在仿真模式下开…

每日一个MySQL知识点:主从表大小相差巨大和一个BUG

一、主从相同表空间相差巨大 1.1 问题描述 我们知道MySQL主从基本上是逻辑的复制&#xff0c;那么有少量的空间差异没有问题&#xff0c;但是本案例主库表只有10G&#xff0c;但是从库表有100G&#xff0c;这么大的差距比较少见&#xff0c;需要分析原因。 1.2 问题分析 实…

ResNet (深度残差网络)

ResNet 算法概述 解决的核心问题&#xff1a;网络的退化现象 网络层数在变深之后&#xff0c;性能不如浅层时候的性能 。注意&#xff1a;网络退化既不是梯度消失也不是梯度爆炸。 那是如何解决退化现象的呢&#xff1f;引入残差模块 把模型的输入分成两条路&#xff1a;右边…

SQL 大全(四)|数据库迁移升级时常用 SQL 语句

作者 | JiekeXu 来源 |公众号 JiekeXu DBA之路&#xff08;ID: JiekeXu_IT&#xff09; 如需转载请联系授权 | (个人微信 ID&#xff1a;JiekeXu_DBA) 大家好&#xff0c;我是 JiekeXu,很高兴又和大家见面了,今天和大家一起来看看SQL 大全&#xff08;四&#xff09;|数据库迁移…

由浅入深了解 深度神经网络优化算法

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 导言 优化是从一组可用的备选方案中选择最佳方案。优化无疑是深度学习的核心。基于梯度下降的方法已经成为训练深度神经网络的既定方法。 在最简单的情况下&#xff0c;优化问题包括通过系统地从允许集合中…

Jenkins+GitLab+Docker搭建前端自动化构建镜像容器部署

前言 &#x1f680; 需提前安装环境及知识点&#xff1a; 1、Docker搭建及基础操作 2、DockerFile文件描述 3、Jenkins搭建及基础点 &#x1f680; 目的&#xff1a; 将我们的前端项目打包成一个镜像容器并自动发布部署&#xff0c;可供随时pull访问 一、手动部署镜像及容器 1…

6-《网络面试》

6-《网络面试》 1.http是什么&#xff1f;http的工作机制&#xff1f;http报文&#xff1f;1.1 http工作机制&#xff1a;1.2 URL和http报文 2. HTTP请求方法和状态码3.Get和Post的区别4.HTTP的Header解析1.text/html2.x-www-form-urlencoded3.multipart/form-data4.applicatio…

中断相关概念并利用中断实现按键点亮LED灯

一.中断相关概念 什么是中断&#xff1f; 中断是指计算机运行过程中&#xff0c;出现某些意外情况需主机干预时&#xff0c;机器能自动停止正在运行的 程序并转入处理新情况的程序&#xff0c;处理完毕后又返回原被暂停的程序继续运行。 什么是EXTI&#xff1f; 外部…

【vue上传文件——hash】

vue上传文件 要求:只能上传视频,先计算文件的hash值,hash值一样则不需要上传,不一样在执行上传 分析:因为el-upload没有找到合适的属性,本次用的是原生的input的type属性为file上传 代码: html: 通过点击选取文件按钮调用input上传 js 第一步:点击上传文件先效验是否…

windows下免U盘安装manjaro

建议 建议先看这个https://www.bilibili.com/read/cv23161386/ 背景 新到了一块硬盘&#xff0c;想把这台主机做成双系统的&#xff0c;windowsmanjaro系统。 为什么选择manjaro&#xff1f; win中的虚拟机已经有了日常使用的ubuntu。体验一下manjaro。ubuntu用来开发办公要…

python 之 logging的使用

一、日志模块 import logginglogging.debug("调试日志") logging.info(消息日志) logging.warning("告警日志") logging.error(错误日志) logging.critical(严重错误日志)debug&#xff08;调试&#xff09;级别用于输出调试信息&#xff0c;这些信息主…

JDK源码阅读环境搭建

本次针对jdk8u版本的搭建 1.新建项目 新建java项目JavaSourceLearn &#xff0c;这里我创建的是maven 2.获取JDK源码 打开Project Structure 找到本地JDK安装位置将src.zip解压到项目java包中 整理下项目结构&#xff0c;删除用不到的目录 提示: 添加源码到项目之后首次运行…