首页 > 编程语言 >【C++练级之路】【Lv.23】C++11——可变参数模板、lambda表达式和函数包装器

【C++练级之路】【Lv.23】C++11——可变参数模板、lambda表达式和函数包装器

时间:2024-06-08 09:32:59浏览次数:16  
标签:11 function 函数 int void 练级 C++ 参数 lambda



快乐的流畅:个人主页


个人专栏:《算法神殿》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

一、可变参数模板

C++11更新后,可以创建接受可变参数的函数模板和类模板。

1.1 参数包的概念

以下是基本可变参数的函数模板:

template <class... Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl;
}

void test()
{
 	ShowList();
 	ShowList(1);
 	ShowList(1, 2.4);
 	ShowList(1, 2.4, 'g');
 	ShowList(1, 2.4, 'g', 3.56);
}
  • Args是一个模板参数包,args是一个函数参数包
  • 参数包中可以包含0到任意个模板参数
  • sizeof…运算符可以获取可变参数模板中参数的数量

ps:参数前面有省略号,就是一个可变模版参数,我们把带省略号的参数称为“参数包”
ps:对于可变参数模板,编译器会从函数的实参推断模板参数类型。同时,编译器还会推断包中参数的数目。

1.2 参数包的展开

可变模版参数的一个主要特点,便是无法直接获取参数包中的每个参数,所以只能通过展开参数包的方式来获取参数包中的每个参数。


递归方式:

// 递归终止函数
void _ShowList()
{
	cout << endl;
}
// 展开函数
template <class T, class... Args>
void _ShowList(const T& val, Args... args)
{
	cout << val << " ";
	_ShowList(args...);
}

template <class... Args>
void ShowList(Args... args)
{
	_ShowList(args...);
}

利用子函数_ShowList每次获取参数包的第一个元素并打印,然后继续传递参数包,直到参数包没有参数,调用递归终止函数。


数组方式:

template<class T>
int PrintArgs(T val)
{
	cout << val << " ";
	return 0;
}

template<class... Args>
void ShowList(Args... args)
{
	int arr[] = { PrintArgs(args)...};
	cout << endl;
}

利用数组初始化的过程展开参数包并打印。

以下有一种更简洁的写法,运用了逗号表达式和折叠表达式(C++17)

template<class T>
void PrintArgs(T val)
{
	cout << val << " ";
}

template<class... Args>
void ShowList(Args... args)
{
	(PrintArgs(args), ...);
	cout << endl;
}

1.3 emplace系列

STL容器中emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对于insert系列,emplace系列接口的优势到底在哪里呢?

接口展示:

//emplace系列
template <class... Args>
 	void emplace_back (Args&&... args);
//insert系列
template <class T>
	void push_back (const T& val);
	void push_back (T&& val);

单参数:

void test()
{
	list<my::string> lt;//此处运用my::string,便于调试和观察
	my::string s1 = "1111";
	lt.push_back(s1);
	lt.push_back(move(s1));

	my::string s2 = "2222";
	lt.emplace_back(s2);
	lt.emplace_back(move(s2));

	cout << endl;
	lt.push_back("3333");
	lt.emplace_back("3333");
}

其实,插入s1和s2的过程没有任何区别,有一个细微的区别在于直接插入"3333"时,push_back是构造+移动构造,而emplace_back是构造,相比之下只是少了一个移动构造,差别不大。

那么,为什么emplace_back是构造呢?因为参数包层层往下传递,直到节点的构造函数,在初始化列表中才解析出具体类型,所以就可以直接在节点上进行构造


多参数:

void test()
{
	list<pair<my::string, int>> lt;
	lt.push_back(make_pair("1111", 1));
	lt.push_back({ "2222",2 });

	lt.emplace_back(make_pair("3333", 3));
	lt.emplace_back("4444", 4);
}

因为emplace_back的形参是参数包,所以可以写成多参数的形式传入

二、lambda表达式

2.1 lambda的格式

lambda表达式书写格式

  • [capture-list] (parameters) mutable -> return-type { statement }
  1. [capture-list] : 捕捉列表。捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  2. (parameters):参数列表
  3. mutable:修饰符。mutable可以取消其常量性,让传值捕捉的变量(默认为const)可以被修改。
  4. ->returntype:返回值类型
  5. {statement}:函数体。在该函数体内,可以使用参数和捕获的变量

ps:若不需要传参,则可省略参数列表。
ps:使用mutable修饰,则不可省略参数列表。
ps:返回值类型明确时,可省略,编译器自动推导。

2.2 捕捉列表

捕捉方式分两种,传值捕捉和传引用捕捉。

传值捕捉:

void test()
{
	int x = 1, y = 2;
	//[var]
	auto f1 = [x, y] {cout << x << " " << y << endl; };
	//[=]
	auto f2 = [=] {cout << x << " " << y << endl; };
}

class A
{
public:
	void print()
	{
		//[this]
		auto f3 = [this] {cout << _a1 << " " << _a2 << endl; };
	}
private:
	int _a1, _a2;
};
  • [var]:传值捕捉var变量
  • [=]:传值捕捉所有变量(包括this)
  • [this]:传值捕捉this指针

传引用捕捉:

void test()
{
	int x = 1, y = 2;
	//[&var]
	auto f1 = [&x, &y] {cout << x << " " << y << endl; };
	//[&]
	auto f2 = [&] {cout << x << " " << y << endl; };
}
  • [&var]:传引用捕捉var变量
  • [&]:传引用捕捉所有变量(包括this)

ps:可以混合捕捉,但不能重复捕捉。
ps:只能捕捉父作用域中的局部变量(父作用域,指包含lambda的语句块)。

2.3 lambda的原理

lambda表达式,底层原理就是仿函数(类似于范围for的底层是迭代器)。

先看看以下代码:

void test()
{
	auto f1 = [](int x) {cout << x << endl; };
	f1(1);
	cout << typeid(f1).name() << endl;

	auto f2 = [](int x) {cout << x << endl; };
	f2(2);
	cout << typeid(f2).name() << endl;
}

对于用户,lambda是匿名函数对象,所以用auto接收。但是即使定义完全相同的两个lambda,其函数类型还是不同。所以 lambda表达式之间不能相互赋值,它们之间的类型是互不相同的。


再看看以下代码:

class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};

void test()
{
	double rate = 0.49;
	// 函数对象
	Rate r1(rate);
	r1(10000, 2);
	// lambda
	auto r2 = [=](double monty, int year)->double 
		{return monty * rate * year;};
	r2(10000, 2);
}

使用方式相同:

  • 函数对象将rate作为其成员变量,在定义对象时给出初始值即可。
  • lambda表达式通过捕获列表可以直接捕获到该变量。

底层实现相同:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

2.4 lambda的优势

对于一个商品类Goods:

struct Goods
{
	string _name;
	double _price;
	int _evaluate;

	Goods(const char* str, double price, int evaluate)
		: _name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

我们想按不同方式取比较,进行排序。

仿函数:

struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

void test()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
}

lambda表达式:

void test()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });
}

仿函数和lambda表达式比较:

  • 仿函数的方式比较笨重,且类名必须取的清晰,否则容易混淆不清。
  • lambda表达式则简洁清晰,在原本传参的位置定义,可以清楚知道比较的方式。

三、函数包装器

目前,我们学习了三种回调函数的方式:函数指针、仿函数和lambda表达式。而在传参时,函数模板会因不同的类型而产生多份实例化,导致效率低下。所以,我们希望有统一的类型可以接收可调用对象,以达到包装的目的,提高效率。

3.1 function

function是一个模板类,提供了一种通用的、多态的函数封装,是一个函数包装器(适配器)

template <class T> function;     // undefined
template <class Ret, class... Args> class function<Ret(Args...)>;

ps:Ret是被调用函数的返回类型,Args是被调用函数的参数类型。


请看看以下代码:

void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

struct SwapFunctor
{
	void operator()(int& x, int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	}
};

void test()
{
	//函数指针
	function<void(int&, int&)> f1 = Swap;
	//函数对象
	function<void(int&, int&)> f2 = SwapFunctor();
	//lambda表达式
	function<void(int&, int&)> f3 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
}

function提供了统一的类型来接收不同类型的可调用对象,包括函数指针、仿函数和lambda表达式等,实现了函数包装。


比较特殊的,是function接收类的成员函数:

class Plus
{
public:
	static int plusi(int x, int y)
	{
		return x + y;
	}

	double plusd(double x, double y)
	{
		return x + y;
	}
};

int main()
{
	//类的静态成员函数
	function<int(int, int)> f1 = Plus::plusi;
	//类的普通成员函数
	function<double(Plus*, double, double)> f2 = &Plus::plusd;
	Plus p;
	f2(&p, 1.1, 2.2);

	function<double(Plus, double, double)> f3 = &Plus::plusd;
	f3(Plus(), 1.1, 2.2);
	return 0;
}
  • function接收类的成员函数,要& + 类域(静态成员函数不用&)。
  • 因为类的成员函数有隐含的参数this指针,所以function内部的类型要加上类指针。
  • 由于传入指针比较麻烦,所以编译器做了特殊处理,可以传入类。

leetcode 150.逆波兰表达式求值

逆波兰表达式(function化简版):

class Solution
{
public:
    int evalRPN(vector<string>& tokens)
    {
        stack<int> st;
        unordered_map<string, function<int(int, int)>> hash = 
        {
            {"+", [](int x, int y){return x + y;}},
            {"-", [](int x, int y){return x - y;}},
            {"*", [](int x, int y){return x * y;}},
            {"/", [](int x, int y){return x / y;}}
        };

        for(auto& str : tokens)
        {
            if(hash.count(str))
            {
                int right = st.top();st.pop();
                int left = st.top();st.pop();
                st.push(hash[str](left, right));
            }
            else st.push(stoi(str));
        }

        return st.top();
    }
};

3.2 bind

bind是一个模板函数,可以接收一个可调用对象,进行函数参数绑定,返回一个绑定后的对象

//simple(1)	
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);

ps:fn是可调用对象,args是需要进行绑定的参数列表。


bind的绑定方式分两种,绑定值和绑定占位符

int Sub(int x, int y)
{
	return x - y;
}

void test()
{
	function<int(int, int)> f1 = Sub;
	f1(10, 5);
	//绑定值——调整参数个数
	function<int()> f2 = bind(Sub, 20, 10);
	f2();
	//绑定占位符——调整参数顺序
	function<int(int, int)> f3 = bind(Sub, placeholders::_2, placeholders::_1);
	f3(10, 5);
	//同时绑定值和占位符
	function<int(int)> f4 = bind(Sub, 20, placeholders::_1);
	f4(10);
}
  • placeholders是与bind一起使用的工具,用于指定绑定表达式中的占位符。
  • _1代表函数调用实参的第一个位置,依此类推至 _n。
  • 每次函数调用时,传入的实参会对应到bind的参数列表,再传入调用的函数。
  • 绑定值后,function内的类型要相应的变化(参数类型个数要减少)

对于之前function接收类的成员函数,我们可以用bind进行化简:

class Plus
{
public:
	double plusd(double x, double y)
	{
		return x + y;
	}
};

void test()
{
	//function<double(Plus, double, double)> f3 = &Plus::plusd;
	//f3(Plus(), 1.1, 2.2);

	function<double(double, double)> f3 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
	f3(1.1, 2.2);
}

运用bind将Plus()参数固定绑死,这样在传参时就不用每次传入,变得更加简洁。


真诚点赞,手有余香

标签:11,function,函数,int,void,练级,C++,参数,lambda
From: https://blog.csdn.net/2301_79188764/article/details/139249863

相关文章

  • 【第11章】SpringBoot实战篇之文章(下)含条件分页
    文章目录前言一、文章列表查询1.ArticleController2.ArticleService二、文章查询1.ArticleController2.ArticleService三、文章更新1.ArticleController2.ArticleService四、文章删除1.ArticleController2.ArticleService五、文章列表查询(条件分页)1.Artic......
  • 类和对象(二)(C++)
    初始化列表classDate{public:Date(intyear,intmonth,intday){_year=year;_month=month;_day=day;}private:int_year;int_month;int_day;};虽然上述构造函数调用之后,对象中已经有了一个初......
  • Day17| 110.平衡二叉树、 257. 二叉树的所有路径 、 404.左叶子之和
    110.平衡二叉树(优先掌握递归)再一次涉及到,什么是高度,什么是深度,可以巩固一下。题目链接/文章讲解/视频讲解:https://programmercarl.com/0110.平衡二叉树.html#Definitionforabinarytreenode.#classTreeNode:#def__init__(self,val=0,left=None,right=None):......
  • 07c/c++面向对象
    07C/c++零碎语法目录文章目录07C/c++零碎语法C1.封装2.继承3.多态C++4.1封装4.1.1封装的意义5.继承5.1继承的基本语法5.2继承方式5.3继承中的对象模型5.4继承中构造和析构顺序5.5继承同名成员处理方式5.6继承同名静态成员处理方式5.7菱形继承C1.封......
  • c++类与对象
    classBook{public:charname;intpages;voidprintpages();}voidprintpages(){std::cout<<"书的页数"<<endl;}在定义类的属性和方法时可以选择1.public:公有成员(在程序中类的外部是可访问的)。2.protected:受保护成员(在类的外部是不可访问的,甚......
  • c++的面对对象 的 虚函数
    #include<iostream>#include<string>classAnimal{public:virtualvoidmakeSound()=0;//纯虚函数,所有的动物都应该提供自己的叫声};classDog:publicAnimal{public:voidmakeSound()override{std::cout<<"Woof!"<&l......
  • 【C++修行之道】类和对象(五)日期类的实现、const成员、取地址及const和取地址操作符重
    目录一、日期类的实现Date.h 1.1GetMonthDay函数(获取某年某月的天数) 问:这个函数为什么不和其他的函数一样放在Date.cpp文件中实现呢?1.2CheckDate函数(检查日期有效性)、Print函数(打印日期)1.3实现日期类的逻辑运算符重载<运算符的重载 ==运算符重载其他运算符重载......
  • C++AB类相互包含
    本文讨论我们该如何实现如下结构,而不会报错:classA{Bb;}classB{Aa;}我们知道编译器的执行顺序是从上到下的,当我们在A中使用B时,B还没有定义,甚至没有声明,所以在这里我们先给代码加上一段前向声明。classB;classA{Bb;}前向声明让编译器知道B类是......
  • 一篇文章带你搞懂C++引用(建议收藏)
    引用6.1引用概念引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"typedef是给类型取别名引用是给变量取别名注意:引用类型必须和引用实体是同种类......
  • 最大公约数(gcd())和最小公倍数(lcm())的c语言和c++详细解法
    最大公约数(gcd())和最小公倍数(lcm())最大公约数:定义:两个或多个整数共有的约数中最大的一个。例如:整数12和18,他们的公约数有1、2、3、6,其中最大的公约数是6。c语言解法:辗转相除法和更相减损法1、辗转相除法:思路:先求解较大的数除以较小的数的余数,再用较小的数除以前......