目录

前言:

1 lambda表达式

2 模板的可变参数

3 默认成员函数控制


前言:

继上文介绍了右值概念,本文介绍两个C++11中的重要概念,lambda表达式和模板的可变参数,这两个部分都不算难,重在理解,有了lambda表达式和模板的可变参数的基础才好理解包装器。


1 lambda表达式

常见的,想要排序一个数组,可以利用库里面的sort函数,利用仿函数来排序,排序的方向由仿函数的实现来决定:

int main()
{
	int arr[10] = { 1,4,2,6,9,7,0,8,3,5 };
	sort(arr, arr + sizeof(arr) / sizeof(int));
	for (auto e : arr)
	{
		cout << e << " ";
	}
	return 0;
}

这是普通的排序,那么我们想要实现降序,我们就要写一个仿函数:

struct Less
{
	bool operator()(int a,int b)
	{
		return a > b;
	}
};
int main()
{
	int arr[10] = { 1,4,2,6,9,7,0,8,3,5 };
	sort(arr, arr + sizeof(arr) / sizeof(int),Less());
	for (auto e : arr)
	{
		cout << e << " ";
	}
	return 0;
}

但是问题就来了,如果排的是自定义类型呢?如果我们传的是指针但是想要按照指针解引用之后的类型来比较呢?

自定义类型也一样,我们同样可以使用仿函数来解决:

struct Less
{
	bool operator()(const A& a, const A& b)
	{
		return a._b > b._b;
	}
};
int main()
{
	vector<A> Aa = { {"c",4},{"a",2}, {"b",3}, {"e",1} };
	sort(Aa.begin(), Aa.end(), Less());
	return 0;
}

但是问题又来了,如果一个类,有很多很多变量,我们难道就要写这么多个仿函数吗?成员变量一多,能比较的变量一多,代码看起来就有点冗余了,这时候,lambda表达式就出场了:

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

lambda表达式各部分的介绍:

1 capture-list是捕获列表,不可以省略

2 parameters是函数参数列表,没有参数传递就可以省略

3 mutable是一个修饰词,默认情况下lambda表达式是const函数,mutable可以取消const性质,不可以省略

4 return-type是返回值,可以省略

5 statement是函数体,不可以省略

以上是关于lambda表达式的介绍,怎么感觉越看越像函数呢?

对咯,lambda表达式的本质就是仿函数,不信咱们一会儿可以看看汇编。

那么根据lambda表达式的定义,可以分为省略和不可以省略的部分,省略的有参数列表和mutable以及return-type可以省略,其他都不可以省略。

那么用法是什么呢?咱们先来写出来看看:
 

int main()
{
	[](const A& a1, const A& a2)->bool { return a1._b > a2._b; };
	return 0;
}

就看起来还像模像样的,调用却成为了问题,此时,auto就要闪亮登场了,因为它的类型实在过于复杂,我们先用auto来接受,再用typeid打印看看:

int main()
{
	auto Fun = [](const A& a1, const A& a2)->bool { return a1._b > a2._b; };
	cout << typeid(Fun).name() << endl;
	return 0;
}

 这个类型有够吓人的吧?实际上整个表达式都相等,打印出来的结果也都是不一样的:

int main()
{
	auto Fun1 = [](const A& a1, const A& a2)->bool { return a1._b > a2._b; };
	auto Fun2 = [] { cout << " " << endl; };
	auto Fun3 = [] { cout << " " << endl; };
	cout << typeid(Fun1).name() << endl;
	cout << typeid(Fun2).name() << endl;
	cout << typeid(Fun3).name() << endl;
	return 0;
}

这点就有点像符号表修饰的那种感觉,lambda表达式的类型你不知我不知,只有编译器才知道,怎么个知道法呢?在vs2019里面打印出来是lambda后面有很长的字符串,2022的是这样,但是具体怎么修饰的我们不得而知,可以知道的是2019后面的那一长串字符串是uuid:

算法实现,我们就不用深究了。

那么现在了解了lambda表达式,我们怎么使用呢?

如下:

int main()
{
	vector<A> Aa = { {"c",4},{"a",2}, {"b",3}, {"e",1} };
	auto Fun = [](A a1, A a2)->bool { return a1._b > a2._b; };
	sort(Aa.begin(), Aa.end(), Fun);

	return 0;
}

这样可以完成和上面一样的效果,当然,返回值可以不用写,因为编译器会自动推导,那么我们也可以:

int main()
{
	vector<A> Aa = { {"c",4},{"a",2}, {"b",3}, {"e",1} };
	auto Fun = [](A a1, A a2)->bool { return a1._b > a2._b; };
	sort(Aa.begin(), Aa.end(), Fun);

	sort(Aa.begin(), Aa.end(), [](A a1, A a2) 
		{
			return a1._b < a2._b;
		}
	);
	return 0;
}

这样调用也是没有问题的。

那么最基本的用法已经介绍完了,现在来介绍,捕获列表 Mutable 以及底层。

前面提及,捕获列表是不能省略的,那么顾名思义,捕获嘛,捕捉当前作用域的同名的变量:

int main()
{

	int a = 2, b = 1;
	auto Fs = [a, b]() { int tmp = a; a = b; b = tmp; };
	Fs();
	return 0;
}

[]里面捕捉当前局部域里面的同名的变量,捕捉了之后我们是否可以进行交换呢?答案是不可以:

因为这两个a b 不是一样的,并且lambda本身具有const的性质,所以我们需要mutable来改变,但是改变了之后也就没有改变a b的值:

int main()
{
	int a = 2, b = 1;
	auto Fs = [a, b]() mutable{ int tmp = a; a = b; b = tmp; };
	Fs();
	cout << a << " " << b << endl;
	return 0;
}

打印出来的结果依然是2 和 1 ,这是因为捕获列表里面实际上是 a b 的拷贝,所以在里面交换了值是不会对外面的 a b起到作用的。

所以呢,想要交换,我们可在参数列表部分使用引用即可,现在了解一下捕获的一些其他用法:


int main()
{
	int a = 2, b = 1,c = 2,d = 3;
	auto Fs1 = [=]()mutable { a++, b++, c++, d++; };
	auto Fs2 = [&](){ a++, b++, c++, d++; };
	auto Fs3 = [&a]() { a++; };
	auto Fs4 = [&, a]() { b++,c++,d++ ; };
	return 0;
}

这里我们可以总结为:

1 [=]以传值的方式捕获局部域中的所有变量

2 [&]以引用的方式捕获局部域中的所有变量

3 [&,val]以引用的方式捕获局部域中的所有变量,除了val

4 [&val]以引用的方式捕获局部域中的val

既然是捕获,可以捕获显式的,也可以捕获隐式的,比如this指针,[this]代表捕获this指针

也可以进行混合捕获,比如[= , &a,&b],以引用的方式捕捉a b,其他都以传值的方式捕捉,但是呢,不允许传值捕获两次[=,a],这样就出问题了,也即是说不能用两次相同的捕获方式捕获同一变量。

那么现在就简单看一下lambda的底层:

 看看咯,底层依旧是仿函数。


2 模板的可变参数

模板的可变参数,在C++11中可以经常看到的:

就比如emplace的参数,就是模板的可变参数,没错,那三个点也算进去了!

其实我们很早很早就看到过了:

有思考过printf为什么可以一次性打印多个参数吗?因为模板的可变参数,在C语言里面可以一次性打印多个值(只用一次printf),那么我们想用C++实现怎么办呢?

首先简单介绍一下模板的可变参数的基本概念:

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void Cpp_Printf(Args... args)
{

}

Args是一个模板参数包,用模板参数包来定义函数形参参数包,那么什么是函数形参参数包呢?

在printf里面,参数包的底层是一个数组,但是C++里面,参数包我们只能说它是一个集合,所以当我们传了参数进去,但是不能像使用for遍历一个数组一样去遍历这个参数包了。

虽然但是模板的可变参数是有点难用的,就像这样:

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

int main()
{
	Cpp_Printf(1, "aaa", 231);
	return 0;
}

我们要看参数包有多少个数据,可以使用sizeof打印,但是打印的语法呢,就是sizeof后面加上...,这个看起来有点奇怪。

现在引入一个问题,如何打印参数包里面的东西呢?

可以使用递归方式去打印,但是呢,递归条件是什么?

平常使用递归的时候,函数体的行为都是运行时确定的,比如哪个变量变成了多少多少就停止什么的。但是模板的可变参数是编译时确定的,这也就意味我们不能使用参数包的大小来确定什么时候停止,所以我们就可以调用子函数:

void _Cpp_Printf()
{
	cout << endl;
}

template <class T, class ...Args>
void _Cpp_Printf(const T& val, Args... args)
{
	cout << val << endl;

	_Cpp_Printf(args...);
}

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

int main()
{
	Cpp_Printf(1, "aaa", 231);
	return 0;
}

编译器的调用是先调用只有一个模板参数包的函数,然后是有值模板的函数,最后参数包的数据个数为0了,就走最上面的函数,此时函数调用就结束了。

所以在这里编译器的行为就是要在编译期间将整个递归的过程推导完,更不能使用sizeof来判断是否递归结束了,当然了,这里容易迷惑的一个点就是...的位置。

整个编译器的行为可以说是,调用一次函数函数,从参数包里面拿出一个值,然后实例化出对应的函数,调用完了重复这个过程,直到模板参数列表为空了,这就结束即可。

相关的实例化的函数就像:

void _Cpp_Printf(const std::string & val)
{
	cout << val << endl;
	_Cpp_Printf();
}

void _Cpp_Printf(const char& val, std::string z)
{
	cout << val << endl;
	_Cpp_Printf(z);
}

void _Cpp_Printf(const int& val, char y, std::string z)
{
	cout << val << endl;
	_Cpp_Printf(y, z);
}

void Cpp_Printf(int x, char y, std::string z)
{
	_Cpp_Printf(x, y, z);
}

那么以上是递归方式展开参数包的,也就是说只要能展开函数参数包,就可以实现函数的调用,还有一种方式是逗号表达式展开函数参数包

template<class T>
int Func(const T& t)
{
	cout << t << " ";
	return 0;
}

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

int main()
{
	Cpp_Printf("abc",123,'a');
	return 0;
}

arr的大小是不能确定的,因为编译器在编译期间就要推导参数包里面有几个参数,有几个,arr就开多大,因为是int数组,所以对应的函数Func也要写成int,这点要注意。

实际编译器推导成的话就是:

void Cpp_Printf(int x, char y, std::string z)
{
	int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) };
	cout << endl;
}

那么它的优势在哪里呢?

比如list的emplace_back,用到的就是模板可变参数,这里的话,简单介绍一下过程,相对于push_back来说,push_back可以接受左值也可以接受右值,同样的,模板的可变参数也可以接受左值右值,那么这里来看的话,插入一个确定的数据,就没有太大的可比性了。

但是:

int main()
{
    list<pair<bit::string, int>> lt;
	pair<string, int> kv1("xxxxx", 1);
	pair<string, int> kv2("xxxxx", 1);
	lt.emplace_back(kv2);
	lt.emplace_back(move(kv2));
	lt.emplace_back("xxxxx", 1);
	return 0;
}

 emplace传参如果是直接pair类型,那么pair类型要构造一次吧?此时string构造一次,传进去的时候,右值引用,会调用一次string的移动构造吧?(在new节点的时候),但是如果传的是参数包,就会像上面的递归一样,参数包打开,编译器实例化出对应的函数,然后直到new节点,才开始构造pair,此时string初始化了一次,也就是只有一次构造,所以说,emplace传参数包的时候调用一次构造,传确定的数据的时候,构造 + 移动构造,但是呢,移动构造本身代价就很小,所以对于自定义来说效率提升没有那么好,相反的,浅拷贝的在这里还好点

所以模拟实现list的时候,实现emplace_back版本就需要:

template <class... Args>
void emplace_back(Args&&... args)
{
	emplace(end(), forward<Args>(args)...);
}
template <class... Args>
void emplace(iterator pos, Args&&... args)
{
	Node* cur = pos._node;
	Node* newnode = new Node(forward<Args>(args)...);
	Node* prev = cur->_prev;
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	_size++;
}
template <class... Args>
ListNode(Args&&... args)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(forward<Args>(args)...)
{}


3 默认成员函数控制

目前final override default都是已经介绍了的,这里只是介绍一个delete关键字。

final override在多态章节有所介绍,default在红黑树部分有所介绍,使用场景就像,写了拷贝构造,就不生成默认的构造了,所以红黑树本体的成员变量节点就无法构造,就需要强制生成默认构造函数。

delete的作用是不希望能强制调用该函数,在C++98里面常见的做法是将函数设为私有等,比如不希望能直接实例化一个对象,就可以将构造函数私有,但是还有问题是,我们可以通过static修饰的方法直接进行通过类域来访问公有函数从而达到实例化对象,但是拷贝构造没有禁止,这就意味着可以任意new对象了,在C++11的解决方法是加入delete,并且只声明不实现

class Heap
{
public:
	static Heap* CreateObj()
	{
		return new Heap;
	}
	// C++98 私有+只声明不实现
	// // C++11
	Heap(const Heap&) = delete;
private:
	Heap(const Heap&);

	Heap()
	{}
	int _a = 1;
};

当我们实现打印的时候,也会发现io流的参数必须加引用,不然就会报错,这就是因为io流里面的拷贝构造函数就设置为了delete了。


感谢阅读!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐