一、认识左值右值
1. 临时对象、匿名对象、字面常量等右值不能被取地址
2. 变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变
量表达式的属性是左值。
int b = 1;
int&& rrx1 = move(b);
// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
int&& rrx2 = move(rr1);
3. 加不加const一般和&有关(左值引用)
4.拷贝构造函数:复制资源(深拷贝),而右值引用构造函数(移动构造)是转移资源(浅拷贝 + 资源接管)
5.左值引用不能直接引用右值,但是const左值引用可以引用右值;
右值引用不能直接引用左值,但是右值引用可以引用move(左值)
move(左值):变成右值后调用移动构造,原来左值对象的资源被掠夺了
std::move 的本质是什么?
std::move :核心作用是 “授权” 编译器使用移动语义(调用移动构造 / 移动赋值),但不会触发任何构造、析构或内存释放,自然也不会影响原对象的生命周期。
对象的生命周期由其创建方式(如栈上局部对象、堆上 new 对象、全局对象)和作用域规则决定,与是否被 std::move 转换无关:若原对象是栈上局部对象(如 string s = "hello";),其生命周期仍到当前作用域结束(如函数返回、块结束),std::move(s) 不会让它提前销毁;
若原对象是堆上对象(如 string* p = new string("hello");),其生命周期仍由 delete p 决定,std::move(*p) 也不会让它提前释放;
若原对象是临时对象(如 string("hello")),其生命周期仍遵循 “临时对象在完整表达式结束后销毁” 的规则(除非被引用延长),std::move(string("hello")) 也不会改变这一点。
6.右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:右值引用不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到的左值引用延⻓⽣存期
// r2 += "Test"; // 错误:不能通过到const的引用修改
std::string&& r3 = s1 + s1;// OK:右值引用延⻓生存期
r3 += "Test"; //OK:能通过非const的引用修改
std::cout << r3 << '\n';
return 0;
}
7、移动构造和移动赋值:可以对临时对象等右值资源掠夺,是因为右值被右值引用后这里的s就是一个变量表达式了,是左值,正因为这样所以this指针可以和s交换内容。
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
二、引用折叠
1、使用方法:C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用。C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。下面举出实例:
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{
}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{
}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
通过上面的示例代码:比如f1,只要函数模板参数有左值引用,最后的结果都是左值引用;再看f2,当函数模板参数有右值引用,T为左值就是左值引用,为右值就是右值引用。
上面的例子只是用来理解引用折叠的规则,平常都是编译器自动推导T的类型的,比如下面这个例子:
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
// 所以Function内部会编译报错,x不能++
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
// 所以Function内部会编译报错,x不能++
Function(std::move(b)); // const 右值
return 0;
}
2、完美转发:变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。想要保持t对象的属性,就需要使用完美转发实现,就比如下面这个例子:
void Fun(int& x) { cout << "左值引⽤" << endl; }
void Fun(const int& x) { cout << "const 左值引⽤" << endl; }
void Fun(int&& x) { cout << "右值引⽤" << endl; }
void Fun(const int&& x) { cout << "const 右值引⽤" << endl; }
template<class T>
void Function(T&& t)
{
//Fun(forward<T>(t));
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
return 0;
}
不管Function函数的T是int& 还是 int&&,由于表达式都是左值,传入给 Fun(t) 函数的都是左值,只会匹配void Fun(int& x) 或者 void Fun(const int& x) ,这就无法保持 t 对象原来的属性了。
但是用完美转发 Fun(forward<T> t), 如果传递给Function的实参是右值,T被推导为int,forward内部 t 被强转为右值引用返回;如果传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。
三、lambda 表达式
1、lambda 表达式本质是⼀个匿名函数对象,底层其实是仿函数,⼀般是用auto去接收 lambda 对象。lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }。
[capture-list]:捕捉列表,捕捉列表能够捕捉上下用中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。不可省略。因为编译器是根据[] 来判断接下来的代码是否是lambda表达式。
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连
同 () ⼀起省略。
-> return type:返回值类型,没有返回值时此部分可省略(void)。
{ function boby }:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
using namespace std;
int main()
{
// 一个简单的lambda表达式
//auto add1 = [](int x, int y)->int {return x + y; };
// 1、捕捉为空也不能省略
// 2、返回值可以省略,可以通过返回对象自动推导
auto add1 = [](int x, int y) {return x + y; };
cout << add1(1, 2) << endl;
// 2、参数为空可以省略
// 4、函数体不能省略
auto func1 = []
{
cout << "hello bit" << endl;
return 0;
};
func1();
int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
return 0;
}
2、捕捉列表
lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
- 第⼀种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x, y,&z] 表示 x 和 y 值捕捉,z 引用捕捉。
using namespace std;
int main()
{
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
// 传值捕捉的变量不能修改,引用捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << func1() << endl;
cout << b << endl;
return 0;
}
- 第⼆种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个 = 表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了哪些变量,编译器就会自动捕捉哪些变量。
using namespace std;
int main()
{
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
//// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
//// 隐式引用捕捉
// 用了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
- 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式传值捕捉, x引用捕捉;[&, x, y] 表示其他变量引用捕捉,x和y值捕捉。
using namespace std;
int main()
{
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
// 混合捕捉1
auto func4 = [&, a, b]
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
• lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的局部变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使 用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
• 默认情况下, lambda 捕捉列表传值捕捉的过来的对象不能修改, mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
using namespace std;
int main()
{
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
// 传值捕捉本质是一种拷贝,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外面被捕捉的值,因为是一种“拷贝”
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}

5万+

被折叠的 条评论
为什么被折叠?



