右值引用

左值是指存储在内存中、有明确存储地址(可取地址)的数据 右值是指可以提供数据值的数据(不可取地址)

右值和左值形式

// 左值
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/

只管努力,剩下的交给天意