可变参数模板+lambda

news2024/11/18 2:26:44

目录

可变参数模板

引入

介绍 

展开参数包的方法 

递归

逗号表达式 

整体使用

emplace

介绍

​编辑

使用 

模拟实现

代码

示例

lambda

引入

介绍

格式

使用

传参

捕捉

原理


可变参数模板

引入

  • 还记得c语言中的printf吗,可以传入任意数量的变量来打印,非常的便捷
  • 而他正是用了c中的可变参数列表,不过和我们现在要介绍的可变参数模板的原理不一样
  • 我们只要知道可变参数模板很好用很灵活就是了,它让泛型编程更加完善
  • 但是,c++中的这个语法很抽象,学个基本也就差不多了

介绍 

template <class ...Args>
void ShowList(Args... args)
{}
  • 带...的参数称为"参数包",包含了0-n个模板参数
  • Args是传入的模板参数包,而args是函数形参参数包
  • 但我们无法像printf里那样做,printf是通过va_list对象和宏拿到它的参数的
  • 也无法使用args[i]来取参数
  • 这里,只能通过展开参数包来拿到参数

展开参数包的方法 

递归

// 递归终止函数
template <class T>
void ShowList(const T& t) //当只有一个参数时
{
     cout << t << endl;
}

// 展开函数
template <class T, class ...Args> 
void ShowList(T value, Args... args) //多个参数时
{
     cout << value <<" ";
     ShowList(args...);
}

这里利用了函数重载,当调用showlist函数时:

  • 如果传入了多个参数,会匹配第二个函数,并且不断调用自己,拿到自己当前的第一个模板参数,这样,传入参数包的参数会不断减少一个,直到最后只剩一个模板参数时,调用第一个函数
  • 如果只传入一个参数,刚好就直接匹配到第一个函数(会找到最匹配的那个)
  • 如果要支持无参调用,可以将第一个函数改为无参(因为args可以是0个参数)

逗号表达式 

它不需要递归来获取,而是利用多个语法的特性,直接数组构造过程中把参数包展开

template <class T>
void PrintArg(T t)
{
     cout << t << " ";
}

//展开函数
template <class ...Args>
void ShowList(Args... args)
{
     int arr[] = { (PrintArg(args), 0)... };
     cout << endl;
}
  • 这里是用初始化列表初始化一个数组,而里面的元素是逗号表达式+参数包
  • 所以,实际上它里面的元素展开后是 -- ((printarg(arg1),0), (printarg(arg2),0),(printarg(arg3),0)...)
  • 所以在构造的过程中,会一个一个执行逗号表达式
  • 首先,先执行逗号前的表达式,也就是调用函数,打印参数
  • 然后用逗号后的int值--也就是这里的0,作为该逗号表达式的返回值,初始化数组
  • 这样,一个数组构造完毕,参数包里的参数也都打印出来了

整体使用

但是,上面两种方法只能一个一个取出参数,没啥大用,最多就是打印出来

但是,如果我们能整体使用,就可以用来传参了!

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "Date构造" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


template <class ...Args>
Date* Create(Args... args)
{
	Date* ret = new Date(args...);

	return ret;
}
  • create函数中,将传入的多个参数打包成参数包,直接用来构造date对象
  • 而参数包中的参数,可以用来匹配函数的参数,如果匹配,就可以直接调用该函数
  • 如果不匹配,就会报错
  • 而这个特性,就是emplace的基础
emplace
介绍

  • emplace系列的这两个函数,在多个stl容器中都有设置,而他,正使用了上面介绍的可变参数模板来插入任意个数个元素
使用 

这里我们用list的结点为pair来演示可变参数列表的作用

void test1()
{
    std::list<std::pair<int, bit::mystring>> l;
    l.push_back(make_pair(1, "22"));
    l.push_back({1, "1434"});
    cout << endl;
    l.emplace_back(1, "111");
}
  • 在之前,我们可以使用make_pair/多参数的隐式类型转换 来传入pair对象
  • emplace系列可以直接传参

  • 因为push_back的参数只能是元素类型,也就是这里的pair类型
  • 所以,传入的非pair类型的参数,都得先构造一个临时的pair对象,才能进入该函数

  • 所以 -- 
  • 这里结果的上半截,都是先构造一个string,然后构造pair
  • 再在push_back内部,用pair调用结点的构造函数
  • 在构造函数中,又分别调用pair对象的first成员,second成员的拷贝构造,拷贝出一个pair对象给结点
  • 但又因为有移动拷贝的存在,所以,拷贝给结点的过程是用移动操作完成的

  • 结果的下半截则是用可变参数模板接收,和上面介绍的整体使用一样,参数包一直被传递到调用pair的拷贝构造那里
  • 然后,传入的参数个数和类型刚好能匹配pair的构造函数
  • 所以,就直接调用了string的构造,用传入的字符串初始化了pair的second成员
  • 这样就绕过拷贝操作,直接构造最底层调用

其实这里用自定义类型的话,效率差别不大,因为有移动操作的存在,它可以直接交换资源

但是,如果涉及到占用大内存,但全是内置类型的类,用emplace系列的函数,会大大提高效率(就类似上面的date类)

模拟实现

我们可以在自己实现的list中,也添加emplace函数

代码
        template <class Data>
        ListNode(Data &&val) //之前实现过的移动构造
            : _ppre(nullptr), _pnext(nullptr), _val(forward<Data>(val)){};

        template <class... Args>
        ListNode(Args... args)
            : _ppre(nullptr), _pnext(nullptr), _val(args...) 
        //这样这里将可变参数模板直接传入构造val的构造函数里,如果匹配,就直接构造了
        {
        }


        template <class... Args>   
        void emplace_back (Args&&... args){ 
            insert(end(),args...); //将参数包传给insert
        }


        template <class... Args>  
        iterator insert(iterator pos, Args&&... args)
        {
            PNode cur = pos._pNode;
            PNode pre = cur->_ppre;
            PNode newnode = new Node(args...); //参数包传给node构造函数
 
            newnode->_pnext = cur;
            pre->_pnext = newnode;

            cur->_ppre = newnode;
            newnode->_ppre = pre;

            _size++;

            return newnode;
        }
示例

这里上半截我们比库里的多了两行

原因在于我们这里构造头结点的时候,也需要用构造string,只不过是默认构造;而库里是用的内存池

lambda

引入

我们之前已经学习了很多调用函数的方式,比如:函数指针,仿函数(实际上是类重载了())

  • 但是,这意味着,我们每使用一次函数,就要写一次函数体
  • 如果我们需要多次使用,功能类似但不同的函数该怎么办?定义很多份吗?
  • 这就会涉及到函数起名的问题
  • 功能类似真的好难取名,如果不好好取名,可读性就很差

而这里的lambda表达式,也可以像函数一样使用,它就能解决这个问题

介绍

是C++11引入的一种匿名函数或闭包表达式,它允许你在代码中内联定义函数,而无需显式命名函数

格式

[capture-list] (parameters) mutable -> return-type { statement }

[capture-list] 

  • 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数
  • 捕捉列表能够捕捉上下文中的变量供lambda函数使用
  • 默认情况下,捕捉到的是该变量的拷贝,且为const类型

(parameters)

  • 参数列表,与普通函数的参数列表的使用一致
  • 如果不需要参数传递,可以连同()一起省略

mutable

  • 因为前面已经介绍了,lambda函数默认下,捕捉到的变量是const类型,而mutable可以取消其常量(但注意,这里只是没有了常量性,但依然是变量的拷贝)
  • 使用该修饰符时,参数列表不可省略(即使参数为空)

->returntype

  • 返回值类型,追踪返回类型形式声明函数的返回值类型
  • 没有返回值/返回值明确时可省略(因为编译器可以对返回类型进行推,就像auto那样)

{statement}

  • 函数体
  • 在该函数体内,可以使用参数/捕获到的对象/全局域的函数,其他的无法使用

使用

传参

可以传值,也可以传引用

auto goodsPriceLess = [](const Goods& x, const Goods& y){return x._price < y._price; };

捕捉

默认下捕捉到的是const类型的拷贝

  • 可以使用mutable改变const类型,但依然是拷贝
  • 也可以直接传引用(&变量名),这样x既能修改,也能在外部得到这份修改:
  • int main() {
    	int x = 0;
    	auto a = [&x]() {++x; };
    	a();
    	cout << x << endl;
    	return 0;
    }
  • 如果想要捕捉到该作用域的全部变量时,可以使用[ = ] / [ & ]

  • [ = ] -- 用值捕捉到该作用域的所有变量

  • [ & ] -- 用引用捕捉到该作用域的所有变量

  • 注意!在块作用域之外的lambda表达式,捕捉列表必须为空

原理

  • 实际上,我们的lambda表达式之间是无法赋值的,即使是完全一样的表达式
  • 而原因就是因为,它的底层是仿函数,每一个表达式都是不同的类对象,且类中重载了()运算符
  • 所以,每一个lambda表达式都是不同的类型,所以无法赋值
  • 但是,它支持拷贝

  • lambda表达式都是匿名类对象,他们的名字和类型都是由编译器自动生成的
  • 名字是lambda+uuid(uuid是用了某种算法,生成的重复概率极小的字符串)
  • 而类型则是一个匿名的函数对象类型
  • 使用lambda表达式就是调用类中的operator()函数

  • 如果一个函数指针的类型和lambda表达式类型是一样的,则lambda 表达式可以分配给函数指针(可能因为operator()函数具有与函数指针相似的签名(参数列表和返回类型))

举例

sort(v.begin(), v.end(), [](const Goods& x, const Goods& y) {return x._price > y._price;});

可以传给算法,比如sort中的排序,这样就不用自己专门写一个仿函数了

function包装器(适配器) 

引入

现在,我们已经知道了有三种调用函数的方式 -- 函数指针,仿函数,lambda表达式

那么使用类模板传参的时候,如果分别用了三种不同方式的函数,就会实例化出三份,但实际我们并不需要这么多份

而且如果我们想要将这些可调用类型存在容器中呢?实例化容器的时候类型该怎么写呢?函数指针和仿函数的类型都能写,但lambda的该怎么办呢?

所以,function包装器可以解决这个问题,它可以适配所有可调用的类型

介绍

头文件:<functional>

无需在编译时指定确切的类型,而是在运行时将不同类型的可调用对象分配给它

我们可以用function定义出的对象接收不同类型的可调用对象,只要返回值和参数列表相同即可:

格式

functioni<返回值(参数列表)>

使用 

int func(int x, int y)
{
	return x - y;
}
struct Function
{
	int operator()(int x, int y)
	{
		return x - y;
	}
};
void test2() {
	function<int(int, int)> sub1 = [](int x, int y) {return x - y; };
	function<int(int, int)> sub2 = func;
	function<int(int, int)> sub3 = Function();
	cout << sub1(2, 1) << endl;
	cout << sub2(3, 1) << endl;
	cout << sub3(4, 1) << endl;
}

和原先使用他们的方式一样,只不过接收它的是function类型

存放在容器中:

void test3() {
	vector< function<int(int, int)>> f;
	f.push_back(func);
	f.push_back(Function());
	f.push_back([](int x, int y) {return x - y; });
	cout << f[0](1, 2) << endl;
}

当我们存起来后,可以通过下标拿到可调用对象,再使用它执行函数功能

除此之外,可以用来指定命令执行某一函数

比如:当我们已经有了后缀表达式时,就可以直接进行计算,但是得分很多种情况,运算符越多,情况也越多

  • 如果直接使用判断的方式,效率太低
  • 我们可以利用function和map,直接拿到传入运算符的运算方式,然后传参即可
  • int evalRPN(vector<string>& tokens) {
      stack<int> st;
      map<string, function<int(int, int)>> opFuncMap =
     { 
     { "+", [](int i, int j){return i + j; } },
     { "-", [](int i, int j){return i - j; } },
     { "*", [](int i, int j){return i * j; } },
     { "/", [](int i, int j){return i / j; } }
     };
    
      for(auto& str : tokens)
     {
             if(opFuncMap.find(str) != opFuncMap.end())
             {
                 int right = st.top();
                 st.pop();
                 int left = st.top();
                 st.pop();
                 st.push(opFuncMap[str](left, right));
         }
             else
             {
                 // 1、atoi itoa
                 // 2、sprintf scanf
                 // 3、stoi to_string C++11
                 st.push(stoi(str));
             }
         }
         return st.top();
    }

bind 

介绍

用于创建函数对象(可以是前面提到的那三种)的绑定,允许你在调用函数时 预先绑定一些参数或改变参数顺序
它提供了一种便捷的方法来部分应用函数、改变参数顺序或创建函数对象 (也就是可以自由控制传参) 

std::placeholders

除此之外,bind的使用还需要配合std::placeholders这个作用域中定义的一组占位符对象,它用于指定绑定函数时的参数位置

使用 

修改参数位置

也就是说,这里会存在两层调用

  • 第一层 -- 新调用对象中的第一个参数传给bind中_1对象的位置
  • 第二层 -- 再将该参数传给对应位置的源对象函数形参(也就是这里的a)
  • 这里使用的时候并没有调换两参数的位置,但如果我们将bind中_1和_2的位置交换,即可完成参数位置的修改
为参数传固定值

实际上第一种情况我们用的并不多,更多的是自由地对形参传不同的固定值

double PPlus(int a, double rate, int b) { //这里我们将rate设置为固定传参
	return (a + b) * 1.0 * rate;
}

void test4() {
	function<double(int, int)> new_pplus = bind(PPlus, std::placeholders::_1, 2.3, std::placeholders::_2);
	new_pplus(1, 1);
}

当然,我们可以利用bind生成不同固定传参的变量,而不需要自己去修改函数代码

当我们想要给类的成员函数实现该功能时:

class SubType
{
public:
	static int sub(int a, int b)
	{
		return a - b;
	}

	int ssub(int a, int b, int rate)
	{
		return (a - b) * rate;
	}
};

由于bind函数接收一个函数对象+一系列参数,所以需要依靠类名拿到这两种函数的函数指针

  • 由于类的静态成员函数可以直接通过类域调用,所以函数指针就是subtype::sub
  • 而普通的成员函数必须要拿到地址才行,也就是&subtype::ssub
  • 但实际在使用时,都可以在前面+&,这样就不用可以去区分了
  • 最重要的一点!!!类的成员函数会将this指针隐式传入,而这个指针不是我们手动传入的,而是通过可调用对象,调用到该函数,然后将该对象的指针作为this指针

 

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

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

相关文章

pytorch,tf维度理解RNN

input_t input_t.squeeze(1) 这行代码用于从 input_t 中去除尺寸为1的维度。在深度学习中&#xff0c;经常会出现具有额外尺寸为1的维度&#xff0c;这些维度通常是为了匹配模型的期望输入维度而添加的。 在这里&#xff0c;input_t可能具有形状 (batch_size, 1, feature_dim…

青否数字人SaaS系统5.0发布,支持真人接管实时驱动!

青否数字人SaaS系统5.0正式发布&#xff0c;提供口播视频批量制作7*24小时直播全套解决方案。同时直播间支持真人开麦/输入文字选择音色接管&#xff0c;实时驱动直播间数字人回复。 7*24小时直播 青否数字人客户端选择克隆好的数字人主播&#xff0c;克隆好的声音&#xff0…

实现vue项目和springboot项目前后端数据交互

1、安装node.js 太高版本的win7不支持 这里安装node-v12.16.2-x64.msi&#xff0c;指定安装位置后直接按下一步就可以。npm是node内置的工具 这里配置npm的镜像cnpm&#xff08;提高下载速度&#xff0c;以后用到npm的命令都可以用cnpm命令替换&#xff09;不指定cnpm版本使用…

Tuxera2023最新版本新功能特性

当您获得一台新 Mac 时&#xff0c;它只能读取 Windows NTFS 格式的 USB 驱动器。要将文件添加、保存或写入您的 Mac&#xff0c;您需要一个附加的 NTFS 驱动程序。Tuxera 的 Microsoft NTFS for Mac 是一款易于使用的软件&#xff0c;可以在 Mac 上打开、编辑、复制、移动或删…

【论文阅读笔记】 Curated Pacific Northwest AI-ready Seismic Dataset

Curated Pacific Northwest AI-ready Seismic Dataset 太平洋西北部人工智能地震数据集 摘要 描述了一个AI就绪地震数据集包括各种地震事件参数 仪器元数据 地震波行描述地震目录和事件属性&#xff08;事件震级类型&#xff0c;信道类型&#xff0c;波形极性&#xff0c;信…

【框架源码篇 02】Spring源码-手写DI

Spring源码手写篇-手写DI 简单回顾前面的手写IoC的内容。 一、DI介绍 DI(Dependency injection)依赖注入。对象之间的依赖由容器在运行期决定&#xff0c;即容器动态的将某个依赖注入到对象之中。说的直白点就是给Bean对象的成员变量赋值。 在这里我们就需要明白几个问题。 1.…

【进阶C语言】C语言文件操作

1. 为什么使用文件 2. 什么是文件 3. 文件的打开和关闭 4. 文件的顺序读写 5. 文件的随机读写 6. 文本文件和二进制文件 7. 文件读取结束的判定 8. 文件缓冲区 一、文件与文件的意义 1.文件的意义 文件的意义&#xff0c;无非就是为什么要使用文件&#xff1f; &#xff08;1&…

当中国走进全球化的“深水区”,亚马逊云科技解码云时代的中国式跃升

中国跨境贸易中支付金融与服务领域的综合创新型企业连连国际的联席CEO沈恩光发现&#xff0c;眼下&#xff0c;很多跨境电商的出海方式已发生了变化。几年前&#xff0c;它们还主要借助第三方电商平台&#xff0c;而现在&#xff0c;更多公司开始选择通过自主渠道进入海外市场&…

线性代数-Python-01:向量的基本运算 - 手写Vector及numpy的基本用法

文章目录 一、代码仓库二、向量的基本运算2.1 加法2.2 数量乘法2.3 向量运算的基本性质2.4 零向量2.5 向量的长度2.6 单位向量2.7 点乘/内积&#xff1a;两个向量的乘法 --答案是一个标量 三、手写Vector代码3.1 在控制台测试__repr__和__str__方法3.2 创建实例测试代码3.3 完整…

磁盘显示脱机状态如何处理

问题描述&#xff1a; 在磁盘管理可以看到磁盘1是脱机状态 脱机&#xff08;由于管理员设置的策略&#xff0c;该磁盘处于脱机状态&#xff09; 解决方法一&#xff1a;CMD窗口处理 1.打开CMD&#xff0c;运行diskpart 2.输入san&#xff0c;打开san策略&#xff0c; 输入san…

2防火墙:基础知识

看了一下相关视频&#xff0c;感觉要会防火墙&#xff0c;还是得补一下网络基础。看一下谢希仁的《计算机网络》&#xff0c;应该主要看网络层就可以。网络以前学的还可以&#xff0c;但现在也就能记得不到50%&#xff0c;正好重新学习一下&#xff0c;或许会有新的感受。 看完…

S4.2.4.3 Electrical Idle Sequence(EIOS)

一 本章节主讲知识点 1.1 EIOS的具体码型 1.2 EIOS的识别规则 1.3 EIEOS的具体码型 二 本章节原文翻译 当某种状态下,发送器想要进入电器空闲状态的时候,发送器必须发送EIOSQ,也既是:电器Electrical Idle Odered Set Sequence。当然,除非在某些情况下,特殊制定,也是…

用这个方法,谁都可以刷到leetcode排名第一(可复制)

前几天&#xff0c;有人分享了一个利用GPT在leetcode刷题&#xff0c;学习算法&#xff0c;启迪思路&#xff0c;提升编程能力的方法。 开始还不信&#xff0c;自己试了一下&#xff0c;惊了&#xff01;AI理解问题&#xff0c;编码解决问题的能力现在已经这么流弊了吗&#xf…

【具身智能综述1】A Survey of Embodied AI: From Simulators to Research Tasks

论文标题&#xff1a;A Survey of Embodied AI: From Simulators to Research Tasks 论文作者&#xff1a;Jiafei Duan, Samson Yu, Hui Li Tan, Hongyuan Zhu, Cheston Tan 论文原文&#xff1a;https://arxiv.org/abs/2103.04918 论文出处&#xff1a;IEEE Transactions on E…

正则表达式之学习笔记

正则表达式学习笔记 一、概念二、正则表达式组成三、常见的正则表达式3.1 .匹配任意字符3.2 * 匹配前一个字符的0个或多个实例3.3 ^ 匹配输入字符串的开头3.4 $ 匹配行尾3.5 [] 匹配字符集合\<\> 精确匹配符号 一、概念 正则表达式是由一系列特殊字符组成的字符串&#…

大数据分析实践 | pandas数据质量分析

文章目录 &#x1f4da;数据质量评估的五个维度&#x1f4da;口袋妖怪数据质量分析&#x1f407;导入库和数据&#x1f407;检查数据&#x1f407;缺失值分析&#x1f407;重复值检测&#x1f407;异常值检测 &#x1f4da;数据质量评估的五个维度 Coherent: without semantic …

Jetson nano 安装Ubuntu20.04系统

一、下载Ubuntu20.04镜像 下载地址&#xff1a;点击 二、格式化SD卡 &#xff08;1&#xff09;工具&#xff1a;SDFormatter &#xff08;2&#xff09;工具下载-百度网盘&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1DcwsGzmqrWwFmzpCV7VCyA 提取码&#xff1a…

【LeetCode】62. 不同路径

1 问题 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&#xff1f…

【Python】13.模块

目录 1. 模块化(module)程序设计理念1.1 模块和包概念的进化史1.2 标准库模块(standard library)1.3 为什么需要模块化编程1.4 模块化编程的流程1.5 模块的API 和功能描述要点1.6 模块的创建和测试代码1.7 模块文档字符串和API 设计 2. 模块的导入2.1 import 语句导入2.2 from……

Spark--经典SQL50题

目录 连接数据库准备工作 1、查询"01"课程比"02"课程成绩高的学生的信息及课程分数 2、查询"01"课程比"02"课程成绩低的学生的信息及课程分数 3、查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩 4、查询平均成绩…