您现在的位置是:首页 >技术教程 >【C++】C++11常用新特性网站首页技术教程
【C++】C++11常用新特性
目录
C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节主要讲解实际中比较实用的语法。
一、统一的列表初始化
在C++11中,我们可以使用{}
列表初始化所有的自定义类型和内置类型,相比于在C语言学习阶段{}一般只用于初始化数组和结构体的方式,C++11实现了统一的列表初始化,同时初始化方式也可以将=
进行省略
列表初始化方式: 同时C++11对于new[]申请的空间,可以直接初始化
struct Point
{
int _x;
int _y;
};
int main()
{
//用初始化列表初始化时,可以省略 赋值符号=
//以前的{}只能初始化结构体和数组,现在的{}对于自定义类型和内置类型的对象都可以初始化,一切皆可用列表初始化。
int x1 = 1;
int x2{ 2 };//不要这么玩,我们自己能看懂就行
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p1{ 1, 2 };//对结构对象进行初始化,c++11可以省略=
// C++11中列表初始化也可以适用于new表达式中
int* p2 = new int[4]{ 0 };
Point* p3 = new Point[2]{ {1,2}, {3,4} };
return 0;
}
列表初始化对象会自动调用对象所属类的构造函数(在调试界面我们可以发现)
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,下面会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };//构造一个临时对象,然后拷贝构造给d3-->编译器优化为直接构造
return 0;
}
对于自定义类型,需要提供initializer_list类型的构造函数
std::initializer_list类型的初始化是C++11中引入的一种新的语法,它可以用来对任意类型的对象进行列表初始化,也就是使用花括号{}包含一组初始值。
- 当一个类的构造函数接受一个std::initializer_list参数时,可以使用列表初始化来创建该类的对象,例如:
class Foo {
public:
Foo(std::initializer_list<int> list) {
// do something with list
}
};
Foo foo = {1, 2, 3, 4, 5}; // OK
- 当一个函数接受一个std::initializer_list参数时,可以使用列表初始化来调用该函数,例如:
void func(std::initializer_list<int> list) {
// do something with list
}
func({1, 2, 3}); // OK
自定义类型也可以使用列表初始化,只要它们提供了一个接受std::initializer_list参数的构造函数。实际上std::initializer_list是C++11新增的容器类模板,它提供了访问一组const T类型元素的接口,例如size(), begin(), end()等。std::initializer_list对象可以通过花括号{}来创建,也可以通过拷贝或赋值来复制,但是复制时不会复制列表中的元素,而是共享同一个底层数组。
并且在C++11中所有的STL容器(适配器不算)都实现了initializer_list对象的拷贝构造,包括map,set,list,vector,unordered系列等容器在C++11版本中都增加了initializer_list对象的初始化。
实际上initializer_list这个类底层就是一个常量数组,底层接口实现也不复杂,下面我们来模拟封装一个自己的用initializer_list对象拷贝构造vector对象的函数,让模拟实现的vector也支持{}初始化和赋值
namespace wyn
{
template<class T>
class vector {
public:
typedef T* iterator;//vector的迭代器就是原生指针嘛
vector(const initializer_list<T>& l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
//取类模板的内嵌类型记得加typename关键字,因为编译器不知道你取的是类型还是静态变量或函数
while (lit != l.end())
{
*vit++ = *lit++;
}
//for (auto e : l)
// *vit++ = e;
}
vector<T>& operator=(initializer_list<T> l) {
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
int main()
{
wyn::vector<int> v = { 1, 2, 3, 4, 5, 6 };//用initializer_list对象进行vector的拷贝构造
v = { 11, 22, 33, 44, 55, 66 };//用initializer_list对象进行vector的拷贝赋值
}
std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=
的参数,这样就可以用大括号赋值
std::initializer_list的介绍文档:
二、 简化声明
2.1 auto
C++11中已经去除了C++98中auto声明自动类型变量的功能,只可以用来进行变量类型推导
所以就要求我们必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
并且auto仅仅只是占位符,编译阶段编译器根据初始化表达式推演出实际类型之后会替换auto
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
vector<int> f(n+1);
auto g =f;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
auto范围for循环
auto范围for循环是C++11中引入的一种新的语法,它可以用来简洁地遍历一个范围,例如一个容器、一个数组、一个初始化列表等。
for (auto 变量 : 范围) {
// 循环体
}
其中,范围可以是任何可以迭代的对象,例如有begin()和end()成员函数或自由函数的类类型,或者是一个数组类型。变量是一个自动推导类型的变量,它的类型和范围中的元素类型一致,或者是该类型的引用。每次循环时,变量被初始化为范围中的一个元素,直到范围遍历完毕。例如:
std::vector<int> v = {1, 2, 3, 4, 5}; // 定义一个vector容器
for (auto x : v) { // 使用auto范围for循环遍历v中的元素
std::cout << x << " "; // 输出每个元素
}
std::cout << "
";
int a[] = {6, 7, 8, 9, 10}; // 定义一个数组
for (auto& y : a) { // 使用auto范围for循环遍历a中的元素,并使用引用修改它们
y *= 2; // 将每个元素乘以2
}
for (auto z : a) { // 再次使用auto范围for循环遍历a中的元素
std::cout << z << " "; // 输出修改后的每个元素
}
std::cout << "
";
输出:
1 2 3 4 5
12 14 16 18 20
auto范围for循环可以让代码更简洁、也可以避免一些错误,例如下标越界、迭代器失效等。
2.2 decltype
decltype是C++11新增的一个类型说明符,它可以用来获取一个表达式的类型。decltype的用法是:
decltype(表达式)
例如:
int x; // x是int类型
const int& fx(); // fx()返回const int&类型
struct A { double y; }; // A有一个double类型的成员y
A* a = new A(); // a是A*类型
decltype(x) v1; // v1是int类型
decltype(fx()) v2; // v2是const int&类型
decltype(a->y) v3; // v3是double类型
decltype((a->y)) v4; // v4是double&类型,注意括号的影响
此关键字可以帮助我们简化变量的声明,typeid仅仅能获取某一类型的信息,但decltype可以获取变量的类型后,定义出其他的变量,例如在F函数中,我们想用t1 * t2的结果类型定义出一个变量ret,但我们不清楚t1和t2是什么类型,我们不能单独用t1或t2类型定义出变量ret。此时就可以用decltype获取t1 * t2的结果类型,并用这个结果类型定义出变量ret。
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
//decltype()拿到括号里的类型,可以用类型定义出变量 --> 看起来挺牛逼,实际也没啥用
decltype(t1 * t2) ret = t1 * t2;//用t1*t2的类型 定义变量ret,做到了typeid做不到的事情
//typeid().name拿到的是类型的字符串,无法定义变量,只能用来打印
cout << typeid(ret).name() << endl;
cout << ret << endl;
}
int main()
{
const int x = 1; double y = 2.2;
decltype(x * y) ret; // 用x * y的类型double,定义出ret变量
decltype(&x) p; // 用&x的类型int const*,定义出p变量
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');//字符其实就是ascii码
F(x, y);
return 0;
}
实际上,我们可以用auto推导出t1*t2的结果类型,然后用结果类型定义出ret变量,所以decltype看起来花里胡哨的,但其实非常的鸡肋,真不如auto好用。
2.3 nullptr
考虑到NULL底层被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
三、右值引用和移动语义 – 重要
3.1 区分左值引用和右值引用
之前我们就学过引用的语法,从C++11新增右值引用的语法特性后,我们之前学习的引用就叫做左值引用了。但是无论左值引用和右值引用都是在给对象取别名,但它们的使用方式和实现的功能不一样,我们细致来看一下
什么是左值及左值引用?
左值:就是一个可以取地址+可以对它赋值的数据表达式(如变量名或解引用的指针),左值既可以出现在赋值符号的左边,也可以出现在右边。
定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
左值引用:左值引用就是给左值的引用,给左值取别名。
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
什么是右值及右值引用?
右值:右值是不能被取地址,不能被赋值的数据表达式,如字面常量、表达式返回值,匿名对象、函数返回值(这个不能是左值引用返回)等等。
注意:右值只可以出现在赋值符号的右边,不能出现在赋值符号的左边,右值不能取地址
右值引用就是对右值的引用,给右值取别名。用&&
来表示
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
那么区分左值和右值有什么意义呢?
其是C++11区分左值和右值的意义是为了实现移动语义,通过右值引用可以避免无谓的复制,提高程序性能。同时右值引用可以延长右值的生命周期,也可以通过std::move()将左值转换为右值。这些我们下面都会讲解。
那如何通过std::move()将左值转换为右值?
std命名空间中的move函数可以将左值转为右值,move其实就是移动语义,move后的左值变成将亡值。(右值可以分为纯右值和将亡值,纯右值例如字面值,表达式返回值,将亡值例如匿名对象,传值返回的返回值也就是临时对象)
什么是将亡值呢?
例如匿名对象,传值返回的函数调用的返回值等,因为匿名对象在其所在代码行执行完毕后就会被销毁,并且传值返回的函数调用实际利用了中间生成的一个临时变量将返回值从被调用的函数栈帧即将销毁时带出,这个临时变量的值一旦被接收,也就是拷贝或者引用后,此临时变量就会被销毁。所以我们把匿名对象,传值返回的函数调用的返回值称为将亡值,将亡值也是右值。
注意:
1 左值引用只能引用左值,不能引用右值。
但是const左值引用既可引用左值,也可引用右值
2.右值引用只能引用右值,但右值引用可以引用move以后的左值,因为左值move以后,其实就变成右值了。
注意:C++11的move不要轻易使用
move语义的基础是右值引用,它可以绑定到临时对象或将要销毁的对象,从而转移它们的资源。std::move()函数可以将一个左值转换为右值引用,从而触发move语义。但是std::move()并不会真正移动对象,只是改变了对象的类型,真正的移动操作由移动构造函数或移动赋值运算符完成。
只有在确实需要转移资源的时候才使用,否则可能会破坏对象的完整性或导致额外的开销⁴。
3.2 对比左值引用看看右值引用使用价值
先来看看我们之前学的左值引用:
我们之前运用左值引用做函数参数和做函数返回值可以提高效率~
void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
bit::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
但使用过程中我们会发现左值引用也有短板的,如果函数的返回值是左值引用,就要求返回值出了函数作用域不能被销毁,这样才能左值引用返回,减少拷贝提高效率。
但是当函数的返回值是一个局部变量/对象时,我们就用不了引用返回了呀,只能传值返回,如果返回值为内置类型,那拷贝代价还好,如果是一个vector<vector>的二维数组呢?那样就需要深拷贝一个二维数组,并且随着栈帧销毁,原来的二维数组也会被销毁,这样拷贝的资源代价就很大了!那如果返回是红黑树呢,就可能更大了!
为了解决以上问题,C++11更新了右值引用,其价值之一就是补齐左值引用的这部分短板,即当返回值为传值传递时,有可能深拷贝带来的程序效率降低问题。
来看看一个问题理解右值引用和移动语义
我们可以看到str属于局部变量,出函数作用域就销毁了,是右值也是将亡值咯,那传统的左值引用返回,会产生一次深拷贝(视编译器而定,有的编译器会深拷贝两次)
那既然是将亡值了,还要进行深拷贝那是不是有点浪费了呢?所以就需要C++11新增的移动拷贝和移动赋值来提高这样场景下的效率了,我们直接将右值将亡值的资源移动给被拷贝对象或被赋值对象,这样就省去一次深拷贝~而像函数传值返回带来的深拷贝这样的问题此时也就迎刃而解了,如果我们要将传值返回的返回值接收,也就是用返回值初始化接收值时,由于返回值是一个右值,此时不再匹配拷贝构造或拷贝赋值,而是直接匹配移动构造或移动赋值,将右值的空间资源通过交换指针这样的浅拷贝的方式来实现转移,这样就不会进行深拷贝,提高了传值返回接收时带来的深拷贝的问题。
所以 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
// 移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
同样的,移动赋值就是直接将函数返回的右值对象直接赋值给ret对象,此时调用的其实还是移动构造
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
在C++11后其实STL中的容器都是增加了移动构造和移动赋值.
上面所说的右值引用在右值进行拷贝或赋值时,能够减少深拷贝,而是直接将资源进行转移,这其实是右值引用的价值之一,实际右值引用在容器插入数据的时候,也能提高很多的效率。
在C++98里面,下面三行代码的效率是一样的,都需要先进行对象的构造,然后将构造出来的对象作为左值传到链表对象的push_back接口,push_back内部一定会new一个结点出来,将左值参数里的资源重新拷贝一份放到new出来的结点里面。
int main()
{
list<wyn::string> lt;
wyn::string s1("111111");
lt.push_back(s1);
//lt.push_back(move(s1));
lt.push_back(wyn::string("111111"));
lt.push_back("111111");//单参数的隐式类型转换
return 0;
}
但在C++11里面,后两行的代码效率要高于第一行,归根结底就是因为参数不再是左值,而是右值,第二行和第三行代码插入时,传参传的都是匿名对象,也就是右值,实际STL容器除实现移动构造和移动赋值外,还实现了右值引用版本的插入数据接口,意思是当插入数据为右值的时候,不会再调用C++98里面const左值引用版本的插入数据接口了,因为C++11现在实现了右值引用的版本接口,在调用时,如果是右值插入则一定优先调用右值引用版本的接口。在push_back内部new结点的时候,不会重新拷贝一份资源出来,而是直接将右值参数的资源转移到new出来的结点内部,减少一次深拷贝,提高效率。
3.3 万能引用和完美转发(std::forward< T >())
先来看看万能引用 万能引用的作用就是实现完美转发我们后面讲解
&&这样的引用方式我们都知道他是右值引用,但&&在模板中并不代表右值引用,他有新的名词,叫做万能引用,所谓万能引用就是什么类型的参数都可以接收,包括普通左值,普通右值,const左值,const右值,万能引用都可以接收 — 模板的力量
万能引用可以根据传入的参数类型自动推导出左值引用或者右值引用,它的形式是&&,其中T就是被推导的类型。
template<typename T>
void f(T&& param) // param是一个万能引用
{
g(std::forward<T>(param)); // 调用g函数,并完美转发param
}
万能引用的作用是可以实现完美转发,即保持参数的原始值类别的属性(左值或右值)不变地传递给另一个函数。 完美转发需要配合std::forward
函数使用,它可以根据参数的类型选择合适的引用类型返回。
如果我们传入一个左值给f,那么T会被推导为左值引用类型,发生引用折叠,param也就是一个左值引用,std::forward也会返回一个左值引用&,从而保持了参数的原始值类别。
如果我们传入一个右值给f,那么T会被推导为非引用类型,param也就是一个右值引用,std::forward也会返回一个右值引用&&,从而保持了参数的原始值类别。
这样,无论我们传入什么样的参数给f,有了完美转发后,我们可以直接使用完美转发这个函数模板,保持参数原有属性不变。
四、C++11新的类功能
4.1 移动构造函数和移动赋值运算符重载
C++11相较于原来C++类中默认成员函数新增了两个:移动构造函数和移动赋值运算符重载
但要编译器生成这两个默认成员函数,条件会更加苛刻!
如果你自己没有实现移动构造函数,并且没有实现析构函数,拷贝构造,拷贝赋值这三个函数,那么编译器会自动默认生成一个移动构造,该移动构造对内置类型完成逐字节拷贝,对自定义类型需要看该自定义类型是否实现移动构造,如果实现移动构造就调用移动构造,如果没有实现就调用拷贝构造。
如果你自己没有实现移动赋值函数,并且没有实现析构函数,拷贝构造,拷贝赋值这三个函数,那么编译器会自动默认生成一个移动赋值,该移动赋值对内置类型完成逐字节拷贝,对自定义类型需要看该自定义类型是否实现移动赋值,如果实现移动赋值就调用移动赋值,如果没有实现就调用拷贝赋值。
但当内置类型涉及资源申请的时候,编译器默认生成的移动构造或移动赋值是无法完成任务的,所以这种时候需要我们自己写移动构造或移动赋值
4.2 类成员的初始化(缺省值,default,delete)
上面我们看到了编译器默认生成的移动构造和赋值的要求太严苛了,那如果我们不小心破坏了生成的条件,但是还想使用编译器默认生成的该怎么办呢?也很简单,可以用一个关键字default
强制编译器生成默认的成员函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person(Person&& p) = default; //直接强制默认生成!
private:
string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
假设我们现在要实现一个不能被拷贝的类,也就是这个类的对象不允许被拷贝,这种场景叫做防拷贝,某些类的成员并不想被拷贝。我们该怎么解决这个问题呢?
C++98中,可以采用的方式就是拷贝构造函数设置为私有,这样在类外面如果有人想要进行对象的拷贝,他肯定是调不到拷贝构造函数的,这样的解决方式可以防止类外面进行对象的拷贝。
那如果类内的某个公有函数进行了对象的拷贝呢?设置为私有的方式就无法解决了,因为访问限定符只能限制类外,无法限制类内,这样的场景又该怎么办呢?其实还有一种方法,就是只声明不实现,不过这样就会报链接错误,因为编译阶段生成的符号表存的拷贝构造函数的地址是无效的,所以链接阶段通过地址找拷贝构造函数的定义时,就会发生链接错误,所以这样的方式也可以防拷贝。
上面都是C++98的解决方式,C++11中没有这么麻烦,只需要delete
关键字就可以解决,在拷贝构造的声明处加上delete修饰即可,如果有人进行对象的拷贝,则编译期间就会报语法错误。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;//禁用默认构造函数,防止拷贝
private:
string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
五、lambda表达式
5.1lambda表达式的本质
我们之前学过C++的仿函数,仿函数实际是一个重载了operator()的一个类,比如下面进行排序的场景,调用库的sort时,sort函数模板的第三个参数的缺省值就是ComparePriceLess< T >()仿函数对象,默认排升序,如果想要排降序,只需要传一个ComparePriceGreater< T >()仿函数对象即可。
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;
}
};
int main()
{
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());
}
int main()
{
auto add1 = [](int x, int y)->int { return x + y; };
//cout << [](int x, int y)->int {return x + y; }(1, 2) << endl;
cout << add1(3, 2) << endl;
auto add2 = [](int x, int y)
{
return x + y;
};
cout << add2(1, 2) << endl;
return 0;
}
那如果需要比较的性质特别多呢?比如要比较商品的名字,价格,评价等等,并且要实现从小到大和从大到小的仿函数,那我们就需要实现6个仿函数,这样岂不是太繁琐了,写6个struct类,如果类的命名不太好,比如按照1-6来命名类,那看代码的人每看到一个仿函数都需要向上去找对应的仿函数看看具体是什么功能,这样也太麻烦了吧!
为了解决这个问题,C++11就新增了lambda表达式,lambda表达式的本质是一个匿名的函数对象,它可以定义在需要使用的地方,也可以作为参数传递给其他函数。其实lambda底层就是仿函数对象,编译器自动生成了一个lambda的类,并在这个类里面实现了operator(),这个类是编译器自己生成的,每一个lambda的类都是不一样的,执行lambda函数体实际还是执行lambda类里面的operator(),所以本质上lambda和仿函数一样,只不过仿函数的类是我们自己写的,lambda的类是编译器自己随机生成的。
当我们创建一个lambda表达式时,就相当于创建了这个类的一个临时对象。
同时lambda表达式可以捕获外部变量,有两种方式:按值捕获和按引用捕获。
lambda表达式可以返回一个值,也可以返回一个函数对象(即另一个lambda表达式)。 返回值的类型可以由编译器自动推导,也可以由我们程序员显式指定。
那怎么用呢?
5.1 lambda表达式的用法大全
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
其中:
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters)
: 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable
: 默认情况下,lambda传值捕捉变量时,默认是const传值捕捉,mutable可以取消其常量
性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype
: 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}
: 函数体我们写实现过程的地方。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
看看示例:
还是以上面的商品排序为例
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
auto priceLess = [](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._price > g2._price; });//我们可以直接写一个lambda表达式
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {
return g1._evaluate > g2._evaluate; });
for (auto e : v)
{
cout << e._evaluate << endl;
}
}
再深究一下
比如下面的代码,我们想实现一个交换功能的函数,但是上面讲的lambda表达式并不能将x和y进行实际上的交换,因为局部变量出了作用域就销毁了呀,我们可以加 mutable 捕捉外部对象 — 但捕捉过来的对象依旧是外部对象的拷贝,所以还是不能完成交换,此时我们可以用引用捕捉,用法就是&变量名
,这样就能完成对外部对象的交换功能
int main()
{
int x = 0, y = 1;
int m = 0, n = 1;
auto swap1 = [](int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
};
swap1(x, y);
cout << x << " " << y << endl;
//捕捉列表
//传值捕捉 -- 默认
auto swap2 = [x, y]() mutable //加 mutable 可变捕捉 --- 捕捉过来的对象依旧是外部对象的拷贝,所以还是不能完成交换
{
int tmp = x;
x = y;
y = tmp;
};
swap2();
cout << x << " " << y << endl; //传值不能实现
//引用捕捉
auto swap2 = [&x, &y]()
{
int tmp = x;
x = y;
y = tmp;
};
swap2();
cout << x << " " << y << endl;
return 0;
}
我们可以全部引用捕捉或者全部传值捕捉
//全部引用捕捉
auto func2[&]()
{
//...
}
//全部传值捕捉
auto func3 = []()
{
//...
};
当然我们也可以混合捕捉
//混合捕捉 ****
auto func1 = [&x, y]()
{
//...
}
//全部引用捕捉,但x传值捕捉
auto func3 = [&, x]()
{
//...
};
所以lambda相较于仿函数其实就增加了两个新成员,其中最核心的是捕获列表。
最后看看
为什么两个差不多的lambda不能赋值?
因为每次生成的类名不一样 — uuid 底层识别是不一样的类型对象
那uuid是什么?
lambda的uuid是一个编译器生成的唯一的、未命名的非联合类类型,称为闭包类型。-lambda的uuid可以用于判断两个lambda表达式是否相同,或者将lambda表达式转换为std::function对象。lambda的uuid也可以用于模板参数推导,或者作为函数对象传递给STL算法。
[]{}其实也是一个lambda表达式,那一个[]{}lambda对象多大?
一个字节
C++11中,一个lambda对象的大小取决于它捕获了哪些变量,以及是按值还是按引用捕获的。如果没有捕获任何变量,或者只捕获了this指针,那么lambda对象的大小通常是一个字节。如果捕获了多个变量,那lambda对象的大小就可能会增加.