当前位置: 首页 > article >正文

(学习总结20)C++11 可变参数模版、lambda表达式、包装器与部分新内容添加

C++11 可变参数模版、lambda表达式、包装器与部分新内容添加

  • 一、可变参数模版
    • 基本语法及原理
    • 包扩展
    • emplace系列接口
  • 二、lambda表达式
    • lambda表达式语法
    • 捕捉列表
    • lambda的原理
    • lambda的应用
  • 三、包装器
    • bind
    • function
  • 四、部分新内容添加
    • 新的类功能
      • 1.默认的移动构造和移动赋值
      • 2.声明时给缺省值
      • 3.defult 和 delete
      • 4.final 与 override
    • STL中一些变化

以下代码环境为 VS2022 C++

一、可变参数模版

基本语法及原理

C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包。

存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包,表示零或多个函数参数。

我们用省略号来指出一个模板参数或函数参数表示一个包。在模板参数列表中,class… 或 typename… 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟 … 指出接下来表示零或多个形参对象列表。

函数参数包可以用左值引用或右值引用表示,跟上一篇文章讲的普通模板一样,每个参数实例化时遵循引用折叠规则

#include <iostream>
#include <vector>
using namespace std;

template<class ...Args>
class one
{
	;
};

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

template<class ...Args>
void set(const Args&... args)
{
	;
}

template<class ...Args>
void print(Args&&... args)
{
	;
}

int main()
{
	get(2, "111111", 5 + 6);

	set(5.1, 'h', 'e');

	print(1.5, 5.1f, "haha");
	print("hehe");

	one<int, double, long long, char, float> get1;
	one<string, vector<long long>> get2;
	one<long long> get3;

	return 0;
}

可变参数模板的原理跟模板类似,本质还是在编译时实例化对应类型和个数的多个函数

这里可以使用 sizeof… 运算符去计算参数包中参数的个数。

#include <iostream>
using namespace std;

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

int main()
{
	print(1, 2, 3);				// 3个
	print(1.5, 5.1f, "haha");	// 3个
	print();					// 0个
	print("hehe");				// 1个

	return 0;
}

包扩展

对于一个参数包,除了能计算它的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号 … 来触发扩展操作。

#include <iostream>
using namespace std;

void print()				// 当参数为 0 时自动匹配无参 print 递归终止
{
	cout << endl;
}

template<class T, class ...Args>
void print(T&& x, Args&&... args)
{
	cout << x << " ";		// 递归调用 print ,将接收到的第一个参数进行打印
	print(args...);			// 用包扩展将剩下的参数传给下一个 print
}

// 这里用 print(1.5, 5.1f, "haha"); 为例
// 首先会实例化 void print(double x, float y, string z) 函数
// 先打印 x,再通过包扩展将 y, z 传给下一个 print
//void print(double x, float y, string z)
//{
//	cout << x << " ";
//	print(y, z);
//}
// 下一个实例化为 void print(float y, string z) 函数
// 先打印 y,再通过包扩展将 z 传给下一个 print
//void print(float y, string z)
//{
//	cout << y << " ";
//	print(z);
//}
// 下一个实例化为 void print(string z) 函数
// 打印 z,此时包扩展里的参数为 0,会匹配 print() 函数结束包扩展
//void print(string z)
//{
//	cout << z << " ";
//	print();
//}

int main()
{
	print(1, 2, 3);
	print(1.5, 5.1f, "haha");
	print();
	print("hehe");

	return 0;
}

C++11 还支持更复杂的包扩展,直接将参数包依次展开作为实参给一个函数去处理。

#include <iostream>
using namespace std;

template<class T>
int show(T&& x)
{
	cout << x << " ";
	return 0;
}

template<class ...a>
void get(a&& ...x)
{
	;
}

template<class ...Args>
class one
{
public:

	one() = default;
};

template<class ...Args>
void print(Args&&... args)
{
	// 展开发生的位置一般在函数参数列表、成员初始化列表、属性列表、类模版参数列表等
	
	// show(args)... 表示每个参数都会调用一次 show()
	// 若有三个参数可表示为
	// show(one), show(two), show(three)
	// int arr[] = { 0, show(one), show(two), show(three) };
	// get(show(one), show(two), show(three));

	int arr[] = { 0, show(args)... };	// 数组执行顺序是从左到右
	//get(show(args)...);				// 函数参数执行是从右到左
	
	one<Args...> get;				// 类型可在类模板参数列表展开

	cout << endl;
}

int main()
{
	print(1, 2, 3);
	print(1.5, 5.1f, "haha");
	print();
	print("hehe");

	return 0;
}

emplace系列接口

C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均为模板可变参数,功能上兼容 push 和 insert 系列,但是 emplace 还支持新操作,假设容器为container<Type>,emplace 支持直接传入构造 Type 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 Type 对象。

emplace 系列相对于 insert 和 push 系列总体而言是更高效,可以平替后两者。

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<pair<string, int>> arr;

	pair<string, int> get1 = { "1111111111", 1 };

	// 传左值一样走拷贝构造
	arr.push_back(get1);
	arr.emplace_back(get1);

	// 传右值一样走移动构造
	arr.push_back(pair<string, int>("22222222", 5));
	arr.emplace_back(pair<string, int>("22222222", 5));


	// push_back 会先 有参构造 再 移动构造
	arr.push_back({ "333333333", 10 });
	// emplace_back 会将参数直接传进去进行有参构造,没有移动构造,此时效率高一点
	arr.emplace_back("333333333", 10);

	return 0;
}

二、lambda表达式

lambda表达式语法

lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。

lambda 表达式语法使用层而言没有类型,所以一般是用 auto 或者 模板参数定义的对象 去接收 lambda 对象。

lambda表达式的格式: [capture-list](parameters)->return type { function body }

[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。

(parameters) :参数列表,与普通函数的参数列表功能类似,但如果不需要参数传递,则可以将 () 省略。

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

{ function body } :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

#include <iostream>
using namespace std;

int main()
{
	auto add = [](int x, int y)->int { return x + y; };
	cout << add(10, 20) << endl;

	// 1. [] 不能省略
	// 2. () 可以省略
	// 3. ->return type 可以省略
	// 4. {} 不能省略
	auto func1 = [] {};

	func1();

	auto swap = [](int& one, int& two)->void
		{
			int temp = one;
			one = two;
			two = temp;
		};

	int a = 100;
	int b = 1;

	cout << "a == " << a << endl << "b == " << b << endl << endl;
	swap(a, b);
	cout << "a == " << a << endl << "b == " << b << endl;

	return 0;
}

捕捉列表

lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。

  1. 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[ x, y, &z ] 表示 x 和 y 值捕捉,z 引用捕捉。
#include <iostream>
using namespace std;

int main()
{
	int x = 0;
	int y = 10;
	int z = 100;
	int t = 1000;

	auto func1 = [x, y, &z]
		{
			cout << x << " " << y << " " << z << endl;
		};

	func1();

	return 0;
}
  1. 第二种捕捉方式是在捕捉列表中隐式捕捉,在捕捉列表写一个 “ = ” 表示隐式值捕捉,在捕捉列表写一个 “ & ” 表示隐式引用捕捉,这样 lambda 表达式中用了哪些变量,编译器就会自动捕捉哪些变量。
#include <iostream>
using namespace std;

int main()
{
	int x = 0;
	int y = 10;
	int z = 100;
	int t = 1000;

	auto func1 = [=]
		{
			cout << "func1 " << x << " " << y << " " << z << endl;
		};

	auto func2 = [&]
		{
			cout << "func2 " << x << " " << y << " " << z << endl;
		};

	func1();
	func2();

	return 0;
}

  1. 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。当使用混合捕捉时,第一个元素必须是 “ & ” 或 “ = ”,并且 “ & ” 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 “ = ” 混合捕捉时,后面的捕捉变量必须是引用捕捉。[ =, &x ] 表示其他变量隐式值捕捉,x 引用捕捉;[ &, z, y ] 表示其他变量引用捕捉,z 和 y 值捕捉。
#include <iostream>
using namespace std;

int main()
{
	int x = 0;
	int y = 10;
	int z = 100;

	auto func1 = [=, &x]
		{
			cout << "func1 " << x << " " << y << " " << z << endl;
		};

	auto func2 = [&, z, y]
		{
			cout << "func2 " << x << " " << y << " " << z << endl;
		};

	func1();
	func2();

	return 0;
}

lambda 表达式如果在函数局部域中,它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,在 lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

默认情况下, lambda 捕捉列表是被 const 修饰的,则传值捕捉的过来的对象不能修改,mutable 加在参数列表的后面可以取消其常量性,使用该修饰符后,传值捕捉的对象就可以修改了,但是修改的是形参对象,不会影响实参。使用该修饰符后,参数列表即使为空也不可省略

#include <iostream>
using namespace std;

string str1 = "全局变量";

auto func1 = []			// 定义在全局位置,捕捉列表必须为空
	{
		cout << str1 << endl;
		cout << "hello world!" << endl;
	};

void test()
{
	string str4 = "另一个函数中的局部变量";
	static string str5 = "另一个函数中的静态局部变量";
}

int main()
{
	string str2 = "局部变量";
	static string str3 = "静态局部变量";

	auto func2 = [&str2]
		{
			cout << str1 << endl;
			cout << str2 << endl;
			cout << str3 << endl;
			//cout << str4 << endl;		// 无法使用其他局部域变量
			//cout << str5 << endl;		// 无法使用其他局部域静态变量
		};

	func1();
	func2();

	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;

	cout << endl << a << " " << b << " " << c << " " << d << endl;

	auto func3 = [a, b, &c, &d]
		{
			//a = 0;	// 不可修改
			//b = 0;
			d = c = 1000;
		};

	func3();
	cout << a << " " << b << " " << c << " " << d << endl;

	// 传值捕捉是拷贝,并被 const 修饰,
	// mutable 可以去掉 const 属性,使得值捕捉变量可修改
	auto func4 = [a, b, &c, &d]()mutable ->void
		{
			a = 0;		// mutable 修饰后可修改,但不会影响外面被值捕捉的值
			b = 0;
			d = c = -99;
		};

	func4();
	cout << a << " " << b << " " << c << " " << d << endl;

	return 0;
}

lambda的原理

lambda 的原理和 范围for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和 范围for 这样的东西。范围for 底层是迭代器,而 lambda 底层是仿函数对象,也就说如果我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。

仿函数的类名是编译器按一定规则生成的,保证不同的 lambda 生成的类名不同。

lambda 参数 / 返回类型 / 函数体 就是仿函数 operator() 的 参数 / 返回类型 / 函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,如果隐式捕捉,使用哪些就传哪些对象。

lambda的应用

一般使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对也会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。

lambda 在很多地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等。

三、包装器

bind

std::bind 是一个函数模板,它是一个可调用对象的包装器,可以把它看做一个函数适配器,对接收的可调用对象进行处理后返回另一个可调用对象。 std::bind 可以用来调整参数个数和参数顺序

std::bind 在 <functional> 这个头文件中。

调用 std::bind 的一般形式: auto newCallable = std::bind(callable, arg_list);其中 newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。

arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。_1 / _2 / _3… 这些占位符放在 placeholders 的一个命名空间中。

#include <iostream>
#include <iostream>
#include <functional>
using namespace std;

using placeholders::_1;
using placeholders::_2;

int add(int a, int b)
{
	return a + b;
}

struct sub
{
	int operator()(int a, int b)
	{
		return a - b;
	}
};

int main()
{
	// bind 本身返回一个仿函数对象
	auto tadd = bind(add, _1, _2);
	auto tsub = bind(sub(), _1, _2);
	auto tmul = bind([](int a, int b) {return a * b; }, _1, _2);

	int a = 10;
	int b = 5;

	cout << tadd(a, b) << endl;
	cout << tsub(a, b) << endl;
	cout << tmul(a, b) << endl << endl;

	// bind 可以调整对象的参数顺序(但不常用)。
	// _1 代表第一个实参传入对象参数的位置
	// _2 代表第二个实参传入对象参数的位置
	// ... 其它同理
	auto radd = bind(add, _2, _1);
	auto rsub = bind(sub(), _2, _1);
	auto rmul = bind([](int a, int b) {return a * b; }, _2, _1);

	cout << radd(a, b) << endl;
	cout << rsub(a, b) << endl;
	cout << rmul(a, b) << endl << endl;

	// bind 调整对象参数个数(常用)。
	auto sub_10 = bind(sub(), _1, 10);
	auto _100_sub = bind(sub(), 100, _1);

	cout << sub_10(50) << endl;
	cout << _100_sub(-10) << endl;

	return 0;
}

function

std::function 是一个类模板,也是一个包装器。 std::function 的实例对象可以包装存储其他的可调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标会导致抛出 std::bad_function_call 异常。

function 被定义 <functional> 头文件中。std::function - cppreference.com 是 std::function 的官方文件链接。

函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统一类型,对它们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。

#include <iostream>
#include <functional>
using namespace std;

int add(int a, int b)
{
	return a + b;
}

struct sub
{
	int operator()(int a, int b)
	{
		return a - b;
	}
};

struct func
{
	static string getString()
	{
		return "得到字符串";
	}

	int getNumber()
	{
		return 10000;
	}
};

int main()
{
	//function 的 <> 中参数放在 () 里,返回值在 () 的左边:
	function<string()> print = [] { return "hello world!"; };
	function<int(int, int)> tsub = sub();
	function<int(int, int)> tadd = add;

	cout << print() << endl;
	cout << tsub(10, 2) << endl;
	cout << tadd(10, 2) << endl;

	// 对于静态类成员函数
	function<string()> getstr1 = func::getString;
	function<string()> getstr2 = &func::getString;

	cout << getstr1() << endl;
	cout << getstr2() << endl;

	// 对于普通成员函数有麻烦的地方,因为有隐式的 this 指针,调用 function 时需要传入对象
	function<int(func&)> getnum1 = &func::getNumber;
	//function<int(func&)> getnum1 = func::getNumber;	普通成员函数需要取地址符号 '&'

	func one;
	cout << getnum1(one) << endl;

	// 解决方法:使用 bind 将对象的 this 指针进行绑定,方便调用
	function<int()> getnum2 = bind(&func::getNumber, func());

	cout << getnum2() << endl;

	return 0;
}

四、部分新内容添加

新的类功能

1.默认的移动构造和移动赋值

原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 / const 取地址重载,最后重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数 和 移动赋值运算符重载。

如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。则编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

如果自己提供了移动构造或者移动赋值,编译器就不会自动提供拷贝构造和拷贝赋值。

2.声明时给缺省值

这部分内容可参考:(学习总结9)C++学习的初步总结 —— 三、缺省参数

3.defult 和 delete

C++11 可以更好的控制要使用的默认函数。假设要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。如果提供了拷贝构造,就不会生成移动构造,则可以使用 default 关键字显示指定移动构造生成。

如果能想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private,并且只声明,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 = delete 修饰的函数为删除函数。

#include <iostream>
using namespace std;

class One
{
public:

	One() = default;
	~One() = default;

	One(const One&) = default;
	One& operator=(const One&) = default;

	One(One&&) = default;
	One& operator=(One&&) = default;
};

class Two
{
public:

	Two() = delete;
};

int main()
{
	One get1;
	//Two get2;		// 默认构造已经删除,会报错

	return 0;
}

4.final 与 override

这部分内容可参考:
(学习总结17)C++继承 —— 四、派生类的默认成员函数 —— 实现一个不能被继承的类
(学习总结18)C++多态 —— 二、多态的定义及实现 —— 6. override 和 final 关键字

STL中一些变化

下图圈起来的就是 STL 中的新容器,但是实际上常用的是 unordered_map 和unordered_set。

在这里插入图片描述

STL 中容器的新接口也不少,最重要的就是右值引用和移动语义相关的 push / insert / emplace 系列接口 和 移动构造与移动赋值,还有 initializer_list 版本的构造等,容器的 范围for 遍历支持。


http://www.kler.cn/a/507635.html

相关文章:

  • 意图颠覆电影行业的视频生成模型:Runway的Gen系列
  • HunyuanVideo 文生视频模型实践
  • (01)FreeRTOS移植到STM32
  • Web3 时代,区块链与物联网的融合创新前景
  • 工作记录小点
  • 微信小程序订阅消息提醒-云函数
  • 5-1 创建和打包AXI Interface IP
  • 备份和容灾之区别(The Difference between Backup and Disaster Recovery)
  • PDF文件提取开源工具调研总结
  • 国产编辑器EverEdit - 复制为RTF
  • 【vue】rules校验规则简单描述
  • 人工智能之深度学习-[1]-了解深度学习
  • 动态路由vue-router
  • SpringBoot中整合RabbitMQ(测试+部署上线 最完整)
  • 【例43.3】 转二进制
  • Django学堂在线笔记-1
  • FreeRTOS 简介
  • Module 模块
  • 阿里云无影云电脑的使用场景
  • 如何在前端给视频进行去除绿幕并替换背景?-----Vue3!!
  • Redis 性能优化:多维度技术解析与实战策略
  • Java并发编程中的synchronized和volatile:用途解析与使用场景
  • opencv入门基础
  • 分多个AndroidManifest.xml来控制项目编译
  • pikachu靶机-Cross-Site Scripting(XSS)
  • 【大数据】机器学习------支持向量机(SVM)