右值引用&完美转发

一、右值引用

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
    • 一般而言,派生类如果是移动,那么也 期待 基类也是移动构造(派生类、基类的资源一起“偷”)。 但是,以下的写法是不正确的
      CDerived:public CBase 
      { 
      public: 
      CDerived(CDerived&& other) :CBase(other){}
      }
      
      实际上,以上代码 不会 触发 CBase 的移动构造,而是触发的拷贝构造。因为:有名字 的 右值引用 是 左值
    • 所以,当我们期待“基类也是做移动构造时”,应该显式调用 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;
}

参考

posted @ 2023-05-22 15:31  scyrc  阅读(38)  评论(0编辑  收藏  举报