右值引用
左值是指存储在内存中、有明确存储地址(可取地址)的数据 右值是指可以提供数据值的数据(不可取地址)
右值和左值形式
// 左值
int num = 10;
// 左值引用
int &a = num;
// 右值引用
int &&b = 10;
int &&p = b; // 错误
// 常量左值引用
const int &c = num;
const int &g = 10;
const int &g1 = a;
int &t = c; // 错误
// 常量右值引用
const int &&d = 10;
// 常量左值引用
const int &g2 = d;
const int &g3 = b;
const int &&e = b; // 错误
const int &&f = d; // 错误
右值引用
简单来说就是夺舍,就是把临时对象的空间占据,成为管理者,之前的管理者踢出,指向空 然后夺舍之人管理内存空间数据
首先右值的划分:
- 纯右值:非引用返回的临时变量或对象(一般是通过函数的形式)、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
- 将亡值:与右值引用相关的表达式,比如,
T&&
类型函数的返回值、std::move
的返回值等
class Test {
public:
Test():m_num(new int(100)) {
cout << "这是默认构造" << " ";
printf("m_num 地址: %p\n", m_num);
}
Test(const Test& a):m_num(new int(*a.m_num)) {
cout << "这是拷贝构造" << endl;
}
// 移动构造函数(右值引用构造函数)---> 复用另一个对象的资源,夺舍
Test(Test &&a):m_num(a.m_num){
a.m_num = nullptr; // 避免析构释放同一内存,因为夺舍了
cout << "这是移动构造" << endl;
}
~Test() {
cout << "这是析构" << endl;
delete m_num;
}
int *m_num;
};
Test getObj() {
Test t;
return t; // 临时对象的返回
}
int main() {
Test t = getObj();
// 执行结果是如果没写移动构造,就会调用一次默认构造,拷贝构造,两次析构
// 写右值引用的构造,就会调用一次默认构造,移动构造,两次析构
// = 就是调用的移动构造或者拷贝构造
Test t1;
Test t2 = t1;
// 执行结果是调用一次默认构造,拷贝构造,两次析构
// 因为 t1 不是临时对象,临时对象是指在执行当前语句后就结束了,空间就释放了
}
- 在移动构造和拷贝构造都在的时候,就会判断 = 右边的是否是临时对象,是临时对象就调用移动构造,不是就是拷贝构造
- 编译器对于函数返回临时对象,输出结果如果不是上述分析,而是只有默认构造和一次析构,这是因为编译器做的返回值优化
- 当一个函数返回一个局部对象时,编译器可以直接在调用者提供的内存空间中构造这个对象,而不是先在被调用的函数内构造对象然后再拷贝回调用者,这种优化避免了不必要的对象拷贝
- 虽然编译器做了优化,但是调用过程还需知晓
// 续接上
Test &&t1 = getObj();
printf("m_num 地址: %p\n", t1.m_num);
Test t2 = getObj();
printf("m_num 地址: %p\n", t2.m_num);
- 会发现地址一样,说明提供了移动构造函数的临时对象的右值引用有两种写法,可以直接赋值,也可以右值引用
- 如果没有实现移动构造函数,这两种写法都是拷贝构造
// 如果没有移动构造函数,要求更高
// 要求右侧是一个临时的不能取地址的对象
// 测试时把移动构造注释掉
Test getObj1() {
return Test();
}
Test &&t3 = getObj1();
printf("m_num 地址: %p\n", t3.m_num);
// 会发现是正确的,两个地址
// 这两种方式(有和无移动构造函数)不同,第一种是占用了 (具体实现的要占用的堆内存空间),进行管理
// 第二种是使用对象里所有资源
// 更明确的书写方式
Test&& getObj2() {
return Test();
}
Test &&t4 = getObj2();
第二种即使没有移动构造函数也能实现,是因为编译器提供默认的移动构造函数 但是如果手动实现了移动构造函数,会优先调用手动实现的
&& 特性
T&&
和auto&&
都是未定义类型,需要推导,const T&&
一定是右值引用
template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10); // T&& 表示右值引用
int x = 10;
f(x); // T&& 表示左值引用
f1(x); // error, 因为 x 是左值
f1(10); // ok, 因为 10 是右值
int x = 520, y = 1314;
auto&& v1 = x; // auto && 表示左值引用
auto&& v2 = 250; // auto && 表示右值引用
decltype(x)&& v3 = y; // 错误,因为等号左边是右值引用,右边是左值
int&& a1 = 5;
auto&& bb = a1; // 左值引用
auto&& bb1 = 5; // 右值引用
int a2 = 5;
int &a3 = a2;
auto&& cc = a3; // 左值引用
auto&& cc1 = a2; // 左值引用
const int& s1 = 100;
const int&& s2 = 100;
auto&& dd = s1; // 常量左值引用,要加 const
auto&& ee = s2; // 常量左值引用,要加 const
const auto&& x = 5; // 常量右值引用
auto
要满足是右值引用,等号右边只能是右值,比如常数,其他的所有引用型都不能推导出 右值引用 ---> 左值引用
int &&a = 10;
auto&& b = a; // 左值引用
int && b = a; // 报错,编译器认为不匹配
总结
- 左值和右值是独立于他们的类型的,右值引用类型可能是左值也可能是右值
- 编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值
转移和完美转发
move
使用 std::move()
方法可以将左值转换为右值,使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
// 函数原型
template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}
list<string> ls;
ls.push_back("hello");
ls.push_back("world");
list<string> ls1 = ls; // 需要拷贝, 效率低
list<string> ls2 = move(ls);
list<string> &&ls3 = move(ls);
- 这两种方式都可以
- 第一种方式是
move
先将其转换为右值引用,然后ls2
调用移动构造函数,将ls
的所有内容所有权转移给ls2
- 第二种方式则是直接用右值引用,持有
ls
的全部资源 - 如果你需要一个新的对象来拥有原对象的资源,使用
list<string> ls2 = move(ls);
- 如果你需要一个右值引用来延续原对象的资源(比如为了传递给另一个函数),使用
list<string> &&ls3 = move(ls)
forward
右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward()
函数,该函数实现的功能称之为完美转发。
// 函数原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;
// 精简之后的样子
std::forward<T>(t);
- 当
T 为左值引用类型
时,t
将被转换为T
类型的左值
- 当
T 不是左值引用类型
时,t
将被转换为T
类型的右值
template<typename T>
void printValue(T& t) {
cout << "l-value: " << t << endl;
}
template<typename T>
void printValue(T&& t) {
cout << "r-value: " << t << endl;
}
template<typename T>
void testForward(T && v) {
printValue(v);
printValue(move(v));
printValue(forward<T>(v));
cout << endl;
}
int main()
{
testForward(520);
int num = 1314;
testForward(num);
testForward(forward<int>(num)); // 传进去的推导为右值引用,因为不是左值引用,是 int
testForward(forward<int&>(num)); // 传进去的推导为左值引用,因为是左值引用
testForward(forward<int&&>(num)); // 传进去的推导为右值引用,因为是右值引用
}
// 输出结果
l-value: 520
r-value: 520
r-value: 520
l-value: 1314
r-value: 1314
l-value: 1314
l-value: 1314
r-value: 1314
r-value: 1314
l-value: 1314
r-value: 1314
l-value: 1314
l-value: 1314
r-value: 1314
r-value: 1314
解释 testForward 函数,走一遍流程,以第一个 520 为例
- 首先外部传参传的是一个右值引用,
T&&
接收后,v
变成左值引用 printValue(v)
就进入左值引用的实现体,打印l-value:
move (v)
转换成了右值引用,打印r-value:
forward<T>(v)
,由于T
接收的参数是右值引用,因此返回的t
也是右值引用,打印r-value:
说明:参考:https://subingwen.cn/