首页 > 其他分享 >右值引用、转移和完美转发(刨析std::move的实现原理)

右值引用、转移和完美转发(刨析std::move的实现原理)

时间:2024-09-22 10:19:48浏览次数:3  
标签:std 右值 int 刨析 左值 引用 &&

文章目录

0、类型 和 值类别

类型描述了表达式或对象的数据性质(比如int,std::string,注:引用不是独立的类型,而是类型的修饰符

值类别描述的是一个表达式的评估方式,即表达式的”值“在程序中如何表现的。C++中的值类别分为三大类:

  1. 左值
  2. 纯右值
  3. 将亡值

其中,纯右值和将亡值合称为右值

1、左值

  • 定义:左值是指向内存位置的表达式,它代表了一个对象的身份。换句话说,左值是有命名或地址的对象,能够在内存中持久存在,可以被修改。

  • 特点:

    • 可以出现在赋值操作符的左边或右边。
    • 左值可以取地址(&)
    • 通常是变量、数组元素、引用等。代表可以读取和修改的内存区域

    实例:

    int x = 10;    // 这里的 x 是左值
    x = 20;        // x 出现在赋值符号的左边,是左值
    int* p = &x;   // 可以取 x 的地址,因为 x 是左值
    

2、右值

2.1 纯右值

纯右值(prvalue,pure rvalue)是那些表示临时的、不具备持久存储的值

纯右值是不能被修改的常量、临时值、字面量,或者函数的返回值(非引用类型)。

特点:

  • 纯右值是不能赋值的临时值。
  • 通常纯右值用于计算和传递数据。
  • 例如:字面量、算术表达式的结果等。
int a = 5 + 3;      // 5 + 3 产生一个纯右值
int b = 42;         // 42 是一个纯右值

2.2 将亡值

将亡值(xvalue,expiring value)是表示即将被销毁的对象的值类别。

它通常表示一个已经没有必要继续拥有其原本的对象身份的对象。它仍然占有内存,但它即将被销毁或转移。

特点:

  • 将亡值是表示临时对象的值,它即将被销毁。
  • 在使用移动语义时,常见的表达式会产生将亡值。
  • 例如:std::move() 产生的值就是将亡值。
std::string foo() {
    return std::string("hello");
}

std::string str = std::move(foo());  // std::move 产生的亡值

3、左值引用 和 右值引用

声明:左值引用和右值引用是一种类型修饰符,修饰一个已有的类型,使得该类型变成引用类型

  • 左值引用:通过 & 声明,绑定到左值。它只能绑定到左值对象,表示对象的引用。
  • 右值引用:通过 && 声明,绑定到右值。它用于移动语义(Move Semantics)中,帮助提高性能。右值引用允许我们捕获和操作临时对象。

左值引用

左值引用的特性
  • 必须绑定到左值:左值引用只能绑定到左值(有明确存储位置的对象),不能绑定到右值(如字面量、临时对象等)。
int x = 10;
int& ref = x;   // 正确,x 是左值
int& ref2 = 100; // 错误,不能将右值 100 绑定到左值引用
  • 引用的对象可以被修改:通过左值引用可以修改引用的对象。
int a = 10;
int& ref = a;
ref = 20;  // 修改了 a 的值
  • 引用是指向对象的别名:左值引用是对象的别名,操作引用时实际上是在操作引用的对象。
常量左值引用的特性
  • 绑定到右值:编译器为右值 100 创建了一个临时变量,并让 s1 引用这个临时变量,确保在 s1 的作用域内,临时变量的生命周期得以延续。
const int& ref = 100;  // 正确,常量左值引用可以绑定右值

右值引用

  • 右值引用就是对一个右值进行引用的类型。
  • 因为右值是匿名的,所以我们只能通过引用的方式找到它。
  • 无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。

通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

关于右值引用的使用,参考代码如下:

#include <iostream>
using namespace std;

int&& value = 520;
class Test
{
public:
    Test()
    {
        cout << "construct: my name is jerry" << endl;
    }
    Test(const Test& a)
    {
        cout << "copy construct: my name is tom" << endl;
    }
};

Test GetTestObj()
{
    return Test(); // 这里面的是一个右值
}

int main()
{
    int a1;
    int &&a2 = a1;        // error
    Test& t = GetTestObj();   // error
    Test && t = GetTestObj();
    const Test& t = GetTestObj();
    return 0;
}
  • 在上面的例子中int&& value = 520;里面520是纯右值,value是对字面量520这个右值的引用。

  • 在int &&a2 = a1;中a1虽然写在了=右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。

  • 在Test& t = GetTestObj()这句代码中语法是错误的,右值不能给普通的左值引用赋值。

  • 在Test && t = GetTestObj();中GetTestObj()返回的临时对象被称之为将亡值,t是这个将亡值的右值引用。

  • const Test& t = GetTestObj()这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。

4、&&的特性

4.1 函数重载

&& 可以用于区分重载函数,这在需要处理左值和右值时非常有用。我们可以通过定义不同的函数版本来分别处理左值和右值。

另外还有一点需要额外注意const T&&表示一个右值引用,不是未定引用类型。

实例:

template<typename T>
void process_1(T&& param);
void process_2(const T&& param);


int main() 
{
    process_1(10);    // T&& 表示右值引用        
    int x = 10;
    process_1(x);     // T&& 表示左值引用
    process_2(x);    // error, x是左值
    process_2(10);     // ok, 10是右值
}

由于上述代码中存在T&&或者auto&&这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠

在C++11中引用折叠的规则如下:

  • 通过右值推导 T&& 或者 auto&&得到的是一个右值引用类型

  • 通过左值推导T&&或者 auto&&得到的是一个左值引用类型

  • 判断左值引用还是右值引用是需要判断它的值类别,而不是类型

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;
auto&& ee = s2;

const auto&& x = 5;
/*第2行:a1是左值,推导出的bb为左值引用类型
第3行:5为右值,推导出的bb1为右值引用类型
第7行:a3为左值,推导出的cc为左值引用类型
第8行:a2为左值,推导出的cc1为左值引用类型
第12行:s1为常量左值引用,推导出的dd为常量左值引用类型
第13行:s2为常量右值引用,推导出的ee为常量左值引用类型*/

5、转移和完美转发

5.1 std::move

std::move的作用:

  • std::move() 是一个标准库函数,它将一个左值强制转换为右值,从而可以绑定到右值引用上。

  • 使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。

  • 它本质上不会移动任何东西,而只是告诉编译器:“我不再需要这个对象的值了,你可以安全地移动它。”

示例:

#include <iostream>
#include <vector>
#include <utility>  // std::move

int main() {
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2 = std::move(v1);  // 将 v1 的内容移动到 v2

    // v1 现在是空的,资源已经被转移
    std::cout << "v1 size: " << v1.size() << std::endl;
    std::cout << "v2 size: " << v2.size() << std::endl;

    return 0;
}

5.2 剖析move的实现

std::remove_reference::type

std::remove_reference<T>::type 检查类型 T 是否是左值引用(T&)或右值引用(T&&)。如果 T 是引用类型,它会去掉这个引用并返回基本类型;如果 T 不是引用类型,则返回 T 本身。

具体行为:

  • 如果 T 是左值引用(T&),则返回去除左值引用的基本类型 T
  • 如果 T 是右值引用(T&&),则返回去除右值引用的基本类型 T
  • 如果 T 既不是左值引用也不是右值引用,直接返回 T
 // 左值引用类型
using T1 = int&;
using BaseT1 = std::remove_reference::type<T1>;  // BaseT1 是 int

// 右值引用类型
using T2 = int&&;
using BaseT2 = std::remove_reference::type<T2>;  // BaseT2 是 int

// 非引用类型
using T3 = int;
using BaseT3 = std::remove_reference::type<T3>;  // BaseT3 仍然是 int

std::move 的函数原型定义:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type &&>(t);
}

std::move实现,首先,万能引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&T&的引用,获取具体类型T(模板偏特化)。

5.3 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类型的右值

示例:

#include <iostream>
using namespace std;

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));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));

    return 0;
}

测试打印结果:

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);实参为右值,初始化后被推导为一个右值引用

    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v)); forward的模板参数为右值引用,最终得到一个右值,实参为``右值`
  • testForward(num);函数的形参为未定引用类型T&&,实参为左值,初始化后被推导为一个左值引用

    • printValue(v);实参为左值
    • printValue(move(v));通过move将左值转换为右值,实参为右值
    • printValue(forward<T>(v)); forward的模板参数为左值引用,最终得到一个左值引用,实参为左值
  • testForward(forward<int>(num)); forward的模板类型为int,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型

    • printValue(v);实参为左值
    • printValue(move(v));v是左值,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v)); forward的模板参数为右值引用,最终得到一个右值,实参为右值
  • testForward(forward<int&>(num)); forward的模板类型为int&,最终会得到一个左值,函数的形参为未定引用类型T&&被左值初始化后得到一个左值引用类型

    • printValue(v); 实参为左值

    • printValue(move(v));通过move将左值转换为右值,实参为右值

    • printValue(forward<T>(v)); forward的模板参数为左值引用,最终得到一个左值,实参为左值

  • testForward(forward<int&&>(num)); forward的模板类型为int&&,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型

    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值

    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值

    • printValue(forward<T>(v)); forward的模板参数为右值引用,最终得到一个右值,实参为右值

标签:std,右值,int,刨析,左值,引用,&&
From: https://blog.csdn.net/2404_87273268/article/details/142419381

相关文章