Bootstrap

C++11之可变参数模板和lambda表达式

🌈前言

本篇文章进行C++11中可变参数模板和lambda表达式的学习!!!


🚁1、可变参数模板

  • “可变参数模板”是支持任意数量的参数的类模板或函数模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进

  • 然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段,我们掌握一些基础的可变参数模板特性就够我们用了

可变参数的函数模板:

// Args是一个模板参数包,args是一个函数参数包
// 声明一个Args... args参数包,说明这个参数包包含0到任意个模板参数
template <class ...Args>
void ShowList(Args... args)
{}

可变参数模板以两种方式使用省略号:

  • 在参数名的左边,表示”参数包“,它里面包含0到N(N>=0)个”参数包“

  • 在参数名的右边,表示”将参数包扩展为单独的名称(类型对象)“

  • 我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数

  • 由于语法不支持使用args[i]这样方式获取可变参数,所以我们只能用一些奇怪的方式且难理解来一一获取参数包的值


🚂1.1、递归函数方式展开参数包

这种解包方式需要通过”解包终止函数/函数模板“来解决

namespace VT
{
	// 终止函数
	template <typename T>z
	void Test(const T& t)
	{
		cout << t << endl;
	}

	// 展开函数
	template <typename T, typename ...Args>
	void Test(const T& val, Args... args)
	{
		// sizeof ...(args)是求函数参数包的个数 -- 每次进入函数模板解一个,所以一开始为n-1个
		cout << sizeof ...(args) << " " << val << endl;
		
		// 使用递归展开函数参数包 -- 一开始递归调用自身,最后没有参数包为0时,调用另外一个模板函数
		Test(args...);
	}

	void Test()
	{
		Test(1);
		cout << endl;

		Test(1, 'a', 1.0, "abcdef");
		cout << endl;
	}
}

在这里插入图片描述

注意:sizeof …(agrs)是求函数形参的参数包个数,一开始进入函数模板时已经解了一个,所以是3


🚃1.2、逗号表达式展开参数包

逗号表达式:对”对象“赋值时,逗号表达式会按顺序执行逗号前面的表达式,但是只会取最后一个逗号后面的值(int a = (1, 2, 3),取3)

	template <typename T>
	void PrintArg(const T& t)
	{
		cout << t << endl;
	}

	// 展开函数
	template <typename T, typename ...Args>
	void ShowList(const T& t, Args... args)
	{
		cout << t << endl;
		// 逗号表达式从左到右执行,只取最后一个,后面的值,所以Array里面存储的全是0,0之前是展开参数包
		int Array[] = { (PrintArg(args), 0)... };
	}

	void Test()
	{
		ShowList(1, 1.2, 'a', "abcdef");
		cout << endl;
	}

代码解析:

  • 这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数

  • 这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式,所以展开一个参数包就把0赋值给了Array数组,数组有多少个0就代表有多少个参数

expand函数体中逗号表达式解析:

  • (PrintArg(args), 0),也是按照这个执行顺序,先执行PrintArg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(PrintArg(args), 0)…}将会展开成((printarg(arg1),0),(PrintArg(arg2),0), (PrintArg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]

  • 由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分PrintArg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

在这里插入图片描述


不能在类模板中判断递归结束条件,因为模板是编译时才实例化的,并不会执行判断条件(运行时判断)

template <typename T, typename ...Args>
	void ShowList(const T& t, Args... args)
	{
		cout << t << endl;
		if (sizeof ...(args) == 0)
		{
			return;
		}
		int Array[] = { (ShowList(args), 0)... };
	}

	void Test2()
	{
		ShowList(1, 1.2, 'a', "abcdef");
		cout << endl;
	}

在这里插入图片描述


🚄1.3、STL容器中的empalce相关接口函数

vector中emplace_back接口
list中emplace_back接口

template <class... Args>
void emplace_back (Args&&... args);

  • 首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用那么相对insert和
    emplace系列接口的优势到底在哪里呢?
#define _CRT_SECURE_NO_WARNINGS
#pragma once

#include <iostream>
#include <cassert>
#include <cstring>
using namespace std;

namespace mystring
{
	class string
	{
	public:
		typedef char* iterator;
	public:
		iterator begin() { return _p; }
		iterator end() { return _p + _size; }
		//=======================================================================
		string(const char* str = "")
			: _size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(const char* str = "")" << endl;
			_p = new char[_capacity + 1];
			strcpy(_p, str);
		}

		// 移动构造
		string(string&& s) noexcept
			: _p(nullptr)
			, _size(0)
			, _capacity(0)
		{
			this->swap(s);
			cout << "移动构造函数: " << "string(string&& s) noexcept" << endl;
		}
		// 拷贝构造
		string(const string& s)
			: _p(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "拷贝构造: " << "string(const string& s)" << endl;
			string tmp(s._p);
			this->swap(tmp);
		}

		//移动赋值拷贝
		string& operator=(string&& s) noexcept
		{
			cout << "移动赋值拷贝: " << "string& operator=(string&& s) noexcept" << endl;
			this->swap(s);
			return *this;
		}
		// 赋值拷贝
		string& operator=(const string& s)
		{
			cout << "赋值拷贝: " << "string& operator=(const string& s)" << endl;
			string tmp(s);
			this->swap(tmp);
			return *this;
		}

		~string()
		{
			if (_p != nullptr)
			{
				delete[] _p;
				_p = nullptr;
			}
		}
		//=======================================================================
		void push_back(char c)
		{
			insert(_size, c);
		}

		string& operator+=(char c)
		{
			push_back(c);
			return *this;
		}

		void reserve(size_t newCapacity)
		{
			if (newCapacity > _capacity)
			{
				size_t size_tmp = _size;
				char* tmp = new char[newCapacity + 1];
				strcpy(tmp, _p);
				delete[] _p;

				_p = tmp;
				_size = size_tmp;
				_capacity = newCapacity;
			}
		}
		//=======================================================================
		string& insert(size_t pos, char c)
		{
			assert(pos <= _size);
			if (_size == _capacity)
				reserve(_capacity == 0 ? 2 : _capacity * 2);

			size_t end = _size + 1;
			while (pos < end)
			{
				_p[end] = _p[end - 1];
				--end;
			}
			_p[pos] = c;
			++_size;
			return *this;
		}
		//=======================================================================
		void swap(string& s)
		{
			std::swap(_p, s._p);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _p;
		size_t _size;
		size_t _capacity;
	};
}
int main()
{
	// 下面我们试一下带有拷贝构造和移动构造的mystring::string
	// 我们会发现其实差别也不大,emplace_back是直接构造了
	// push_back是先构造,再移动构造,其实也还好
	std::list< std::pair<int, mystring::string> > mylist;
	mylist.emplace_back(10, "sort");
	mylist.emplace_back(make_pair(20, "sort"));

	mylist.push_back(make_pair(30, "sort"));
	mylist.push_back({ 40, "sort" });

	return 0;
}

在这里插入图片描述

总结:

  • emplace_back系列的接口中最大的优点就在与可以利用类本身的构造函数直接在内存之中构建对象,而不需要调用类的拷贝构造函数与移动构造函数

  • emplace系列的接口的是一个可变参数模板,参数是可变参数+万能引用,通过完美转发能准确的识别左值和右值,通过特定的方式解包拿到数据后,可以直接进行构造


🚅2、lambda表达式

🚆2.1、C++98例子

我们写一个自定义类型,然后按它不同的成员进行排序

namespace Lambda
{
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& g1, const Goods& g2)
		{
			return g1._price < g2._price;
		}
	};
	
	struct CompareevaluateLess
	{
		bool operator()(const Goods& g1, const Goods& g2)
		{
			return g1._evaluate < g2._evaluate;
		}
	};

	void Test2()
	{
		vector<Goods> g{ { "苹果", 1.5, 9}, {"雪梨", 1.1, 12}, {"榴莲", 98, 100 } };
		// c++98 -- 使用仿函数进行排序
		sort(g.begin(), g.end(), ComparePriceLess());
		sort(g.begin(), g.end(), CompareevaluateLess());
	}

缺陷:

  • 随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名

  • 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式


🚇2.2、lambda表达式

C++11可以使用lambda表达式来解决上面的问题,可以看出lambda表达式实际是一个匿名函数

void Test2()
{
	// C++11 -- 使用lambda表达式(lambda表达式本身是一个匿名对象,底层是一个仿函数)
	sort(g.begin(), g.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; });
	sort(g.begin(), g.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate < g2._evaluate; });
}

🚈2.3、lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement };
					[]()mutable->{};
  • [capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用

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

  • mutable:默认情况下,lambda函数总是一个const函数(引用捕捉的对象除外),mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)

  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导

  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};

	// 省略参数列表和返回值类型,返回值类型由编译器推导为int,
	// [=]是在该父作用域中以值拷贝方式捕捉全部对象
	int a = 3, b = 4;
	[=] {return a + 3; };

	// 省略了返回值类型,无返回值类型,
	// [&]是在该父作用域中以引用捕捉全部对象
	auto fun1 = [&](int c) {b = a + c; };
	fun1(10);
	cout << a << " " << b << endl;

	// 各部分都很完善的lambda函数,
	// 显示引用捕捉b,其他对象以值拷贝捕捉
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;

	// 显示捕捉x,x为常属性,不能对其做任何修改
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}

在这里插入图片描述

注意:通过上述例子可以看出,lambda表达式实际上可以理解为匿名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量


捕获列表说明:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用

  • [value]:表示值传递方式捕捉变量value

  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)

  • [&value]:表示引用传递捕捉变量value

  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)

  • [this]:表示值传递方式捕捉当前的this指针

注意:

  1. 父作用域指包含lambda函数的语句块

  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割

比如:

  • [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

  • [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

  1. 捕捉列表不允许变量重复传递,否则就会导致编译错误
  • 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  1. 在块作用域以外(全局作用域)的lambda函数捕捉列表必须为空
void Func()
{
	char ch = 'a';
}

auto p = [ch] { cout << ch << endl; }; // error,未定义标识符ch
  1. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都
    会导致编译报错
在这里插入代码片
  1. lambda表达式之间不能相互赋值,即使看起来类型相同(后面剖析底层原理)
void (*PF)();
int main()
{
	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };
	
	// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
	//f1 = f2; // 编译失败--->提示找不到operator=()
	
	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();
	
	// 可以将lambda表达式赋值给相同类型的函数指针(void函数指针)
	PF = f2;
	PF();
	return 0;
}

🚉2.4、函数对象与lambda表达式

函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象

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

	void TT()
	{
		// 函数对象
		double rate = 0.49;
		Rate r1(rate);
		r1(10000, 2);

		// lamber
		auto r2 = [=](double monty, int year)->double 
		{
			return monty * rate * year;
		};
		r2(10000, 2);
	}
  • 从使用方式上来看,函数对象与lambda表达式完全一样

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

在这里插入图片描述

总结:

  • 实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
    果定义了一个lambda表达式,编译器会自动转换成一个仿函数,在该类中重载了operator()

  • lambda类型名由”lambda_+UUID“组成,UUID

在这里插入图片描述

;