侧边栏壁纸
博主头像
Billy 的技术空间博主等级

君子生非异也,善假于物也。

  • 累计撰写 17 篇文章
  • 累计创建 1 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
C++

一文搞懂 C++ 11 右值引用

billy
2021-12-07 / 0 评论 / 3 点赞 / 164 阅读 / 12445 字

1 左值和右值

左值 (lvalue):

  1. 定义: 左值是表达式(不仅是变量)结束后依然存在的对象或函数的引用。简单来说,左值是那些位于赋值符号左边的对象,它们拥有一个明确的内存位置

  2. 特点:

    • 可以在等号的左边(可以被赋值,如果它们不是const)。

    • 有持久的状态(在内存中有确定的位置)。

    • 可以取地址,即能使用 & 操作符。

  3. 示例 1

    int x = 5; // x 是左值
    x = 10;    // 正确,因为 x 是左值

右值 (rvalue):

  1. 定义: 右值是表达式结束后就不再存在的临时对象,或者说是那些不与内存中固定位置相关联的对象

  2. 特点:

    • 通常出现在赋值符号的右边。

    • 不能有赋值给它们。

    • 不具有持久的状态。

    • 不能取地址,即不能对其使用 & 操作符。

  3. 示例 2:

    int x = 5 + 2; // 5 + 2 是一个右值
    10 = x;        // 错误,因为 10 是右值

2 左值引用与右值引用

左值引用(Lvalue Reference)

  1. 定义: 左值引用可以被看作是对象的一个别名,它必须引用一个左值

  2. 语法: 使用 & 符号来定义左值引用。

  3. 特点:

    • 左值引用对象,一旦引用不可更改,且只能在初始化的时候被引用,并且不能被初始化为右值(见下面的示例 3)。

    • 对左值引用的修改会影响原始对象。

  4. 示例 3

    int x = 10;
    int &ref = x;      // 正确, ref 是 x 的一个左值引用 (ref不可再引用其他对象)
    ref = 20;          // 现在 x 也是 20
    int &x1 = 7;       // 错误, x1 不可引用一个右值
    const int &x = 11; // 成功 (const 可以延长右值的生命周期, 让其具备左值属性的效果)

右值引用(Rvalue Reference)

  1. 定义: 右值引用是C++11新引入的,它允许引用临时对象(右值)。这是实现移动语义完美转发的基础。

  2. 语法: 使用 && 符号来定义右值引用。

  3. 特点:

    • 右值引用延长了临时对象(右值)的生命周期。被右值引用绑定的临时对象的生命周期会被延长至与该引用相同(见示例 4)。

    • 右值引用通常用于重载函数,使得函数能根据传入参数是左值还是右值来选择合适的行为(见示例 5)。

    • 右值引用使得编写实现移动语义的类和函数成为可能,这通常可以提高程序的性能,因为它允许资源(例如动态内存)的转移而非复制。

    • 右值引用是一个左值(这可能有点混淆,但是具名对象的右值引用具备左值的所有特点,如果疑惑,可以去开头看看左值的特点)

  4. 示例 4:

    #include <iostream>
    #include <vector>
    
    // 定义一个函数,返回一个临时的std::vector<int>对象
    std::vector<int> create_vector() {
        // 返回一个临时的std::vector<int>对象
        return std::vector<int>{1, 2, 3, 4, 5};
    }
    
    int main() {
        // 使用右值引用接收createVector返回的临时对象
        std::vector<int> &&temp_vector_ref = create_vector();
    
        // 此时,即使create_vector()返回的是一个临时对象,
        // 它的生命周期也会延长,直到temp_vector_ref的生命周期结束
    
        std::cout << "Contents of the vector: ";
        for (int val : temp_vector_ref) {
            std::cout << val << " ";
        }
        std::cout << std::endl;
    
        // temp_vector_ref 可以安全使用,直到它离开其作用域
        return 0;
    }
  5. 示例 5:

    #include <iostream>
    #include <utility>
    
    class Widget {
    public:
        // 一个数据成员,仅用于演示
        int data;
    
        Widget(int d) : data(d) { 
            std::cout << "Widget Constructed with data = " << data << std::endl; 
        }
    
        // 拷贝构造函数
        Widget(const Widget& w) : data(w.data) { 
            std::cout << "Widget Copied with data = " << data << std::endl; 
        }
    
        // 移动构造函数
        Widget(Widget&& w) : data(w.data) { 
            std::cout << "Widget Moved with data = " << data << std::endl; 
            w.data = 0; // 将原对象的数据置为0
        }
    };
    
    // 处理左值的函数
    void process(const Widget& w) {
        std::cout << "Processing lvalue Widget with data = " << w.data << std::endl;
    }
    
    // 处理右值的函数
    void process(Widget&& w) {
        std::cout << "Processing rvalue Widget with data = " << w.data << std::endl;
        // 这里可以安全地修改w,因为它是一个右值引用
        // 例如,可以将资源从w转移到另一个对象
    }
    
    int main() {
        Widget w1(10); // 创建一个Widget对象
        process(w1); // 调用 process(const Widget&),因为w1是左值
    
        process(Widget(20)); // 调用 process(Widget&&),因为Widget(20)是右值
    
        Widget w2(std::move(w1)); // 移动构造一个新Widget,此时w1的资源已被转移
        process(std::move(w2)); // 调用 process(Widget&&),因为std::move(w2)是右值
    
        return 0;
    }

3 移动语义

在上面的示例 5中,我们已经领略了移动语义的用处,所谓移动语义,就是通过 std::move 函数将一个左值变为右值属性,那std::move到底是怎么做到的?我们直接看std::move 的实现:

示例 6:

  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

上面这段代码乍一看似乎晦涩难懂,实际上只是唬人的纸老虎,我们直接看std::move 的返回值, return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); 这个返回值用到了static_cast ,这是我们代码中经常用到的静态类型转换,让我们看看下面的示例 7代码:

示例 7:

std::string s = "c++ 11";
std::string&& s1 = std::move(s); // 正确将 s 变成一个右值,并赋值给右值引用 s1
std::string&& s2 = s1; // 错误,此处的 s1 是一个左值(具名右值引用本身是一个左值,如果疑惑,可以去开头看看左值定义)
std::string&& s2 = static_cast<std::string&&>(s1); // 正确将 s1变成一个右值,并赋值给右值引用 s1

我们编译上面的代码发现 std::move(xxx)static_cast<std::string&&>(xxx) 效果是一样的,所谓的移动语义,就是静态类型转换。那么大家肯定会奇怪,为什么STL要实现复杂的 std::move 函数那?让我们看看std::move 的完整实现:

示例 8:

  /// remove_reference
  template<typename _Tp>
    struct remove_reference
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&>
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&&>
    { typedef _Tp   type; };

  /**
   *  @brief  Convert a value to an rvalue.
   *  @param  __t  A thing of arbitrary type.
   *  @return The parameter cast to an rvalue-reference to allow moving it.
  */
  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

我们可以看到std::move内使用了std::remove_reference 这个结构体,而这个结构体有三个重载实现,再结合这个结构体的名字 remove reference,我们已经知道std::remove_reference 想干什么了,他是要做到无论用户使用什么类型的对象,最终我都会将这个对象的引用&属性去除调,再结合&& 则可以实现将一个不论是左值还是右值属性的对象统统都变成右值属性。

4 万能引用和引用折叠

示例 3中我们通过常量左值引用const int &x = 11,其既可以引用左值,又可以引用右值,是一个几乎万能的引用,但可惜的是由于其常量性,导致它的使用范围受到一些限制。其实在C++11中确实存在着一个被称为“万能”的引用,它看似是一个右值引用,但其实有着很大区别,请看下面的代码:

示例 9:

void foo(int &&i) {}    // i为右值引用

template<class T>
void bar(T &&t) {}        // t为万能引用

int get_val() { return 5; }
int &&x = get_val();      // x为右值引用
auto &&y = get_val();     // y为万能引用

从上面的例子中我们可以看出凡是写出了具体类型的都是右值引用,凡是通过模板或者auto没有具体写明的都是万能引用。为什么会出现这种情况?这里就要说到引用折叠了,请看下面的图一

图一 引用折叠

从上图一中我们很容易得出一个规律,那就是单数引用符号最终都会折叠成为& ,双数引用符号,最终都会折叠成为&& ,有了这个特性我们就知道auto和模板可以通过类型推导的方式将多个引用折叠。因此当使用T&&时,不论T类型是& 还是&& ,使得参数或者返回值都可以被完美的接受,而且不改变T的值属性。那么这个属性的实际用途是什么?请看第五节完美转发。

5 完美转发

在了解完美转发前,我们先来看个例子:

示例 10:

#include <string>

template<class T>
void normal_forwarding(T t)
{
  show_type(t);
}

int main()
{
  std::string s = "hello world";
  normal_forwarding(s);
}

在上面的代码中,函数normal_forwarding是一个常规的转发函数模板,它可以完成字符串的转发任务。但是他是按值转发,也就是说std::string在转发过程中会额外发生一次临时对象的复制。其中一个解决办法是将void normal_forwarding(T t)替换为void normal_ forwarding(T &t),这样就能避免临时对象的复制。不过这样会带来另外一个问题,如果传递过来的是一个右值,则该代码无法通过编译,例如:

示例 11:

#include <string>

template<class T>
void normal_forwarding(T& t)
{
  show_type(t);
}

std::string get_string()
{
  return "hi world";
}

int main()
{
  normal_forwarding(get_string()); // 编译失败
}

那有什么好的办法可以解决这个问题那?接下来让我们的主角登场,std::forward 函数:

示例12:

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

如果已经理解了引用折叠规则,那么上面的代码就很容易理解了。唯一可能需要注意的是return static_cast<_Tp&&>(__t);中的类型转换,之所以这里需要用到类型转换,是因为作为形参的__t是左值。为了让将左右值的属性也返回,这里需要进行类型推导转换

此处用到了std::remove_reference结构体,因此std::forward 有两个重载函数来,分别来接收左值和右值。通过std::remove_reference 将传入参数的引用属性先去除,再附加上对应的&或者&&

当实参是一个左值时,调用的是forward(typename std::remove_reference<_Tp>::type& __t) _Tp实际上是R&,于是static_cast<_Tp&&>被推导为static_cast<R&>

当实参是一个右值时,调用的是forward(typename std::remove_reference<_Tp>::type&& __t) _Tp实际上是R&&,于是static_cast <__Tp&&>被推导为static_cast<R&&>

请注意std::movestd::forward的区别

  1. 其中std::move一定会将实参转换为一个右值引用。

  2. 使用std::move不需要指定模板实参,模板实参是由函数调用推导出来的。

  3. 使用std::forward需要指定模板实参,std::forward会根据左值和右值的实际情况进行转发,(这也是为什么std::forward的实现有两个重载函数的原因)。

3

评论区