右值引用&完美转发
一、右值引用
1. 右值、左值
-
C++ 中表达式分为左值和右值,简单而言,有内存地址的表达式就是左值,它可以出现在赋值语句的左边或者右边。无法取内存地址的表达式是右值,只能出现在赋值语句的右边。
-
左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
-
右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。
void RValue()
{
//auto pos_num = &(10); // 不能取地址 1. 常量字面量
auto pos_char = &("123456"); // "123456" 类型为 const char [7],因此是左值
//auto pos_funA = &(funcA(0x1111)); // 不能取地址 2. 函数调用的返回值
auto pos_funB = &(funcB(0x2222)); // 函数调用返回的类型为左值引用,则返回的结果为左值
//auto pos_class = &(A()); //不能取地址 3. 无名对象
}
-
纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
-
将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。
2.右值引用、左值引用
-
在 C++11 之前,是只有左值引用(C++11之后,为了和右值引用区分,原来的“引用”才称为“左值引用”),没有右值引用的。因此无法用非 const (左值)引用匹配右值的
-
要拿到一个将亡值,就需要用到右值引用:T &&,其中 T 是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活
void RVReference()
{
std::string lv1 = "string,"; // lv1 是一个左值
// std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
std::cout << rv1 << std::endl; // string,
const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
// lv2 += "Test"; // 非法, 常量引用无法被修改
std::cout << lv2 << std::endl; // string,string,
std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
std::cout << rv2 << std::endl; // string,string,string,Test
reference(rv2); // 输出左值
}
3.移动语义
-
传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作, 调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。 试想,搬家的时候是把家里的东西直接搬到新家去,而不是将所有东西复制一份(重买)再放到新家、 再把原来的东西全部扔掉(销毁),这是非常反人类的一件事情。
-
传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题
-
移动构造和移动赋值,统称为 移动语义
class CMyString{
public:
char* m_pBuffer;
int m_iLen;
CMyString(const char* pString){
m_iLen = strlen(pString) + 1;
m_pBuffer = new char[m_iLen];
strcpy(m_pBuffer, pString);
}
~CMyString(){
m_iLen = 0;
if(m_pBuffer){
delete[] m_pBuffer;
}
}
CMyString(CMyString& other){
// 深拷贝
std::cout<<"deep copy"<<std::endl;
this->m_iLen = other.m_iLen;
this->m_pBuffer = new char[m_iLen];
strcpy(this->m_pBuffer, other.m_pBuffer);
}
CMyString(CMyString&& other) noexcept {
std::cout<<"shallow copy"<<std::endl;
this->m_iLen = other.m_iLen;
this->m_pBuffer = other.m_pBuffer; // 浅拷贝、偷资源
other.m_pBuffer = nullptr; // 让 m_pBuffer 不因为other析构而释放
}
CMyString& operator=(const CMyString& other)
{
// 深拷贝
if(this == &other) return *this;
std::cout<<"copy ="<<std::endl;
this->m_iLen = other.m_iLen;
this->m_pBuffer = new char[m_iLen];
strcpy(this->m_pBuffer, other.m_pBuffer);
return *this;
}
// move =
CMyString& operator=(CMyString&& other) noexcept
{
if(this == &other) return *this;
std::cout<<"move ="<<std::endl;
this->m_iLen = other.m_iLen;
this->m_pBuffer = other.m_pBuffer; // 浅拷贝、偷资源
other.m_pBuffer = nullptr; // 让 m_pBuffer 不因为other析构而释放
return *this;
}
};
// 防止编译器优化
CMyString return_rvalue(bool test) {
CMyString a("123"),b("456");
if(test) return a; // 等价于 static_cast<A&&>(a);
else return b; // 等价于 static_cast<A&&>(b);
}
void testString()
{
CMyString str1 = return_rvalue(false);
std::cout << str1.m_iLen<<std::endl;
}
4. std::move()
- C++11 中引入右值引用的同时,还在标准中引入了 std::move 函数。它的作用是『将表达式强行转为右值类型』
- 我们先看下例,改进 myswap 函数:
template<typename T>
void myswap(T& a, T& b){
T temp(a); // 发生拷贝构造
a = b; // 发生拷贝赋值
b = temp; // 发生拷贝赋值
}
template<typename T>
void myswap_move(T& a, T& b){
T temp(std::move(a)); // 发生移动构造
a = std::move(b); // 发生移动赋值
b = std::move(temp); // 发生移动赋值
}
void testStdMove()
{
CMyString str1("123");
CMyString str2("565");
myswap(str1, str2);
myswap_move(str1, str2);
}
4.1 std::move 的使用注意事项
- 组合或者继承时,显式调用 std::move
- 一般而言,派生类如果是移动,那么也 期待 基类也是移动构造(派生类、基类的资源一起“偷”)。 但是,以下的写法是不正确的
实际上,以上代码 不会 触发 CBase 的移动构造,而是触发的拷贝构造。因为:有名字 的 右值引用 是 左值CDerived:public CBase { public: CDerived(CDerived&& other) :CBase(other){} }
- 所以,当我们期待“基类也是做移动构造时”,应该显式调用 std::move
CDerived:public CBase { public: CDerived(CDerived&& other) :CBase(std::move(other)){} }
- 一般而言,派生类如果是移动,那么也 期待 基类也是移动构造(派生类、基类的资源一起“偷”)。 但是,以下的写法是不正确的
- 局部变量返回时,不调用 std::move
现代编译器一般都做 返回值优化,也就是说,与其现在 foo 内部构造一个局部变量 x,再把它复制出去;不如直接在 foo 函数调用的地方直接构造一个 x 对象。这样做的效率显然比移动语义要高。在这类情况下,不用 std::move 为佳
二、完美转发
1.定义
- 完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)
- 比如,以下的工厂模式,很显然是想通过factory 函数把参数,传递给 T 的构造
template<typename T, typename Arg> shared_ptr<T> factory(Arg arg) { return shared_ptr<T>(new T(arg)); }
- 转发参数的窘境 : 因为 arg 一定是左值,无法触发移动语义
void reference(int& v) { std::cout << "左值" << std::endl; } void reference(int&& v) { std::cout << "右值" << std::endl; } template <typename T> void pass(T&& v) { std::cout << "普通传参:"; reference(v); // 始终调用 reference(int&) } int main() { std::cout << "传递左值:" << std::endl; int LValue = 6; pass(LValue); // LValue 是左值, 输出左值 std::cout << "传递右值:" << std::endl; pass(6); // 6是右值, 但输出是左值 return 0; }
2.引用折叠
- 在 C++11 之前,是不允许引用的引用存在的。但是 C++11 之后,引用的引用在特定情况下允许存在,他们会在编译时,被自动化简为左值引用或者右值引用,化简的过程称为 引用折叠
\T& & => T& T& && => T& T&& & => T& T&& && => T&&
- 因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。 更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型
3.万能引用
template<typename T>
void foo(T&& arg)
{
cout << "foo(T&& arg)" << endl;
}
4.std::forward
#include <iostream>
#include <utility>
void reference(int& v) {
std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {
std::cout << " 普通传参: ";
reference(v);
std::cout << " std::move 传参: ";
reference(std::move(v));
std::cout << " std::forward 传参: ";
reference(std::forward<T>(v));
std::cout << "static_cast<T&&> 传参: ";
reference(static_cast<T&&>(v));
}
int main() {
std::cout << "传递右值:" << std::endl;
pass(1);
std::cout << "传递左值:" << std::endl;
int v = 1;
pass(v);
return 0;
}