1:lambda表达式
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda表达式的语法形式简单归纳如下:
[capture](params) opt -> ret {body;};其中capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值类型,body是函数体。
捕获列表[]:捕获一定范围内的变量
参数列表():和普通函数的参数列表一样,如果没有参数参数列表可以省略不写。
auto f = [](){return 1;} // 没有参数, 参数列表为空 auto f = []{return 1;} // 没有参数, 参数列表省略不写opt 选项,不需要可以省略
- mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
- exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用throw();
返回值类型:在C++11中,lambda表达式的返回值是通过返回值后置语法来定义的。
函数体:函数的实现,这部分不能省略,但函数体可以为空。
1.2:捕获列表
lambda表达式的捕获列表可以捕获一定范围内的变量,具体使用方式如下:
- [] - 不捕捉任何变量
- [&] - 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获)
- [=] - 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获)
拷贝的副本在匿名函数体内部是只读的
- [=, &foo] - 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo
- [bar] - 按值捕获 bar 变量, 同时不捕获其他变量
- [&bar] - 按引用捕获 bar 变量, 同时不捕获其他变量
- [this] - 捕获当前类中的this指针
让lambda表达式拥有和当前类成员函数同样的访问权限
如果已经使用了 & 或者 =, 默认添加此选项
#include <iostream> #include <functional> using namespace std; class Test { public: void output(int x, int y) { auto x1 = [] {return m_number; }; // error auto x2 = [=] {return m_number + x + y; }; // ok auto x3 = [&] {return m_number + x + y; }; // ok auto x4 = [this] {return m_number; }; // ok auto x5 = [this] {return m_number + x + y; }; // error auto x6 = [this, x, y] {return m_number + x + y; }; // ok auto x7 = [this] {return m_number++; }; // ok } int m_number = 100; };- x1:错误,没有捕获外部变量,不能使用类成员 m_number
- x2:正确,以值拷贝的方式捕获所有外部变量
- x3:正确,以引用的方式捕获所有外部变量
- x4:正确,捕获this指针,可访问对象内部成员
- x5:错误,捕获this指针,可访问类内部成员,没有捕获到变量x,y,因此不能访问。
- x6:正确,捕获this指针,x,y
- x7:正确,捕获this指针,并且可以修改对象内部变量的值
int main(void) { int a = 10, b = 20; auto f1 = [] {return a; }; // error auto f2 = [&] {return a++; }; // ok auto f3 = [=] {return a; }; // ok auto f4 = [=] {return a++; }; // error auto f5 = [a] {return a + b; }; // error auto f6 = [a, &b] {return a + (b++); }; // ok auto f7 = [=, &b] {return a + (b++); }; // ok return 0; }- f1:错误,没有捕获外部变量,因此无法访问变量 a
- f2:正确,使用引用的方式捕获外部变量,可读写
- f3:正确,使用值拷贝的方式捕获外部变量,可读
- f4:错误,使用值拷贝的方式捕获外部变量,可读不能写
- f5:错误,使用拷贝的方式捕获了外部变量a,没有捕获外部变量b,因此无法访问变量b
- f6:正确,使用拷贝的方式捕获了外部变量a,只读,使用引用的方式捕获外部变量b,可读写
- f7:正确,使用值拷贝的方式捕获所有外部变量以及b的引用,b可读写,其他只读
1.3:返回值
很多时候,lambda表达式的返回值是非常明显的,在C++11中允许省略lambda表达式的返回值。
// 完整的lambda表达式定义 auto f = [](int a) -> int { return a+10; }; // 忽略返回值的lambda表达式定义 auto f = [](int a) { return a+10; };1.4:函数本质
使用lambda表达式捕获列表捕获外部变量,如果希望去修改按值捕获的外部变量,那么应该如何处理呢?
这就需要使用mutable选项,被mutable修改是lambda表达式就算没有参数也要写明参数列表,并且可以去掉按值捕获的外部变量的只读(const)属性。
最后再剖析一下为什么通过值拷贝的方式捕获的外部变量是只读的:
- lambda表达式的类型在C++11中会被看做是一个带operator()的类,即仿函数。
- 按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量值的。
mutable选项的作用就在于取消operator()的const属性。
1.5:Lambda 表达式会产生一个可调用对象
Lambda 表达式会产生一个可调用对象。直接通过 名字() 调用。
它可以转换成函数指针,也可以交给std::function包装,或者交给std::bind绑定参数。
捕获列表没有捕获任何外部变量,作为函数指针使用。
如果捕获列表存在参数,则只能作为仿函数使用,用可调用包装器,绑定器
#include <iostream> #include <functional> using namespace std; void func(int x, int y) { // 1. Lambda 转换成普通函数指针 using Ptr = void (*)(int); Ptr p1 = [](int value) { cout << "函数指针:" << value << endl; }; p1(11); // 2. 使用 std::function 包装 Lambda // [x] 表示保存外部变量 x 的一份副本 function<void(int)> f2 = [x](int value) { cout << "包装器:" << x + value << endl; }; f2(11); // 3. 使用 std::bind 绑定 Lambda 的参数 auto add = [](int first, int second) { return first + second; }; add(10,20); //返回30 // 固定第一个参数为 x,第二个参数留到调用时传入 auto f3 = bind(add, x, placeholders::_1); cout << "绑定器:" << f3(y) << endl; } int main() { func(10, 20); return 0; }第一点:
Ptr p1 = [](int value) { cout << "函数指针:" << value << endl; };这里的 Lambda:
[]:不捕获外部变量。(int value):接收一个整数。- 函数体没有返回结果,所以返回类型是
void。
它的调用形式与Ptr匹配,因此可以转换成函数指针,保存到p1。
第二点:当执行func(10, 20);
f2就可以捕获到x是10,随后调用f2(11);
计算:10 + 11=21.
2:右值引用
左值引用写成T&,右值引用写成T&&。
它们都是给对象起别名,主要区别在于:能绑定什么表达式。
C++11 中右值可以分为两种:一个是将亡值,另一个则是纯右值:
纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等。
| 引用类型 | 示例 | 绑定a | 绑定10 | 能通过引用修改对象 |
|---|---|---|---|---|
| 普通左值引用 | int& | 可以 | 不可以 | 可以 |
| 常量左值引用 | const int& | 可以 | 可以 | 不可以 |
| 右值引用 | int&& | 不可以直接绑定 | 可以 | 可以 |
2.1:基础理解
#include <iostream> #include <utility> // std::move using namespace std; int main() { int a = 10; const int ca = 20; // ==================================== // 1. 普通左值引用:int& // ==================================== int& left = a; // 可以:绑定普通变量 a left = 11; // 可以:通过引用修改 a cout << "a = " << a << endl; // 11 // int& bad1 = 10; // 不可以:普通左值引用不能绑定整数右值 // int& bad2 = a + 1; // 不可以:a + 1 是计算得到的临时结果 // int& bad3 = ca; // 不可以:不能通过普通 int& 引用 const int 对象 // ==================================== // 2. 常量左值引用:const int& // ==================================== const int& c1 = a; // 可以:绑定普通变量 const int& c2 = 100; // 可以:绑定整数右值 const int& c3 = a + 1; // 可以:绑定临时计算结果 const int& c4 = ca; // 可以:绑定 const 变量 cout << "c1 = " << c1 << ", c2 = " << c2 << ", c3 = " << c3 << ", c4 = " << c4 << endl; // c1 = 30; // 不可以:不能通过 const 引用修改对象 // c2 = 40; // 不可以:不能通过 const 引用修改对象 // ==================================== // 3. 右值引用:int&& // ==================================== int&& right1 = 200; // 可以:绑定整数右值 int&& right2 = a + 1; // 可以:绑定临时计算结果 right1 = 300; // 可以:右值引用也能用于修改对象 cout << "right1 = " << right1 << ", right2 = " << right2 << endl; // int&& bad4 = a; // 不可以:表达式 a 是左值 // int&& bad5 = ca; // 不可以:表达式 ca 也是左值 // ==================================== // 4. 使用 std::move // ==================================== int&& right3 = std::move(a); // 可以 // 注意:这里没有创建 a 的副本 // right3 引用的仍然是原来的 a right3 = 50; cout << "a = " << a << endl; // 50 // int& bad6 = std::move(a); // 不可以:普通左值引用不能绑定 std::move(a) 产生的右值表达式 // ==================================== // 5. 有名字的右值引用变量,使用时是左值 // ==================================== int& left2 = right1; // 可以:表达式 right1 是左值 left2 = 400; // 修改 right1 引用的对象 cout << "right1 = " << right1 << endl; // 400 // int&& bad7 = right1; // 不可以:虽然 right1 的类型是 int&&, // 但直接使用名字 right1 时,它是左值表达式 return 0; }| 被绑定的表达式 | int& | const int& | int&& |
|---|---|---|---|
普通变量a | ✅ | ✅ | ❌ |
常量变量ca | ❌ | ✅ | ❌ |
整数值10 | ❌ | ✅ | ✅ |
计算结果a + 1 | ❌ | ✅ | ✅ |
std::move(a) | ❌ | ✅ | ✅ |
已命名的右值引用变量right1 | ✅ | ✅ | ❌ |
2.2:关于右值引用的使用,参考代码如下:
#include <iostream> using namespace std; int&& value = 520; class Test { public: Test() { cout << "construct: my name is jerry" << endl; } Test(const Test& a) { cout << "copy construct: my name is tom" << endl; } }; Test getObj() { return Test(); } int main() { int a1; int &&a2 = a1; // 错误:普通左值引用不能绑定临时对象 Test& t = getObj(); // 正确:右值引用可以绑定临时对象 Test && t = getObj(); const Test& t = getObj(); return 0; }- 在上面的例子中int&& value = 520;里面520是纯右值,value是对字面量520这个右值的引用。
- 在int &&a2 = a1;中a1虽然写在了=右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。
- 在Test& t = getObj()这句代码中语法是错误的,右值不能给普通的左值引用赋值。
- 在Test && t = getObj();中getObj()返回的临时对象被称之为将亡值,t是这个将亡值的右值引用。
- const Test& t = getObj()这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。
注意:
C++ 中的“值”不只包括数字,类的对象也可以按值返回。
Test getObj() { return Test(); }它按值返回Test对象,因此getObj()这个调用表达式是右值。
先看第一行:
Test& t = getObj(); // 错误两边分别是:
Test& t getObj() 普通左值引用 ← 右值普通左值引用不能绑定这个右值,所以编译错误。
如果右边换成已有对象的名字,就可以:
Test obj; Test& t = obj; // 正确:表达式 obj 是左值Test&& t = getObj(); // 正确两边分别是:
Test&& t getObj() 右值引用 ← 右值右值引用允许绑定这个右值,所以正确。
2.3:性能优化
在C++中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。