右值引用、移动语义、完美转发

左值、右值的纯右值、将亡值、右值

左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

右值(rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。

C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

纯右值(prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

将亡值(xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值

将亡值可能稍有些难以理解,我们来看这样的代码:

std::vector<int> foo() {
    std::vector<int> temp = {1, 2, 3, 4};
    return temp;
}
std::vector<int> v = foo();

传统C++的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v, 然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大, 这将造成大量额外的开销 。在最后一行中,v 是左值、 foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到, 而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。 而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。

在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换, 等价于 static_cast(temp),进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。

右值引用、左值引用

右值引用的申明T &&,其中 T 是类型。
右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活

C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,例如:

#include 
#include 
void reference(std::string& str) {
    std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
    std::cout << "右值" << std::endl;
}
int main()
{
    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); // 输出左值
    return 0;
}

rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。

移动语义

移动语义(Move Semantics)是 C++11 引入的一个新特性,用于优化对象的复制和赋值操作。它通过使用 右值引用(Rvalue Reference)和移动构造函数(Move Constructor)、移动赋值运算符(Move Assignment Operator) 来实现。

在传统的实现中,当我们对一个对象进行复制或赋值操作时,通常是将其成员变量逐个拷贝到新的内存空间中,这样会导致大量的内存分配和数据拷贝,严重影响程序的性能

而移动语义可以利用已有的内存空间,或者直接转移对象的所有权,从而避免了不必要的内存分配和数据拷贝,提高了程序的性能。

具体来说,移动语义包括以下几个方面:

  • 右值引用:C++11 引入了一种新的引用类型——右值引用,用于绑定到临时对象或表达式等不可修改的值上。

  • 移动构造函数:移动构造函数是一种特殊的构造函数,用于将右值对象的状态转移到新创建的对象中。移动构造函数以右值引用作为参数,并利用 std::move 函数将其转移。例如:

class MyString {
public:
    // 移动构造函数
    MyString(MyString&& str) noexcept : m_data(str.m_data) {
        str.m_data = nullptr;  // 将原对象指针置为 nullptr,避免二次释放
    }
    // ...
private:
    char* m_data;
};

// 使用移动构造函数创建新对象
MyString s1("hello");
MyString s2(std::move(s1));
  • 移动赋值运算符:移动赋值运算符是一种特殊的赋值运算符,用于将右值对象的状态转移到已有的对象中。移动赋值运算符以右值引用作为参数,并利用 std::move 函数将其转移。例如:
class MyString {
public:
    // 移动赋值运算符
    MyString& operator=(MyString&& str) noexcept {
        if (this != &str) {  // 避免自我赋值
            delete[] m_data;
            m_data = str.m_data;
            str.m_data = nullptr;
        }
        return *this;
    }
    // ...
private:
    char* m_data;
};

// 使用移动赋值运算符进行赋值操作
MyString s1("hello");
MyString s2("world");
s2 = std::move(s1);

通过使用移动语义,我们可以实现高效的对象传递、返回和赋值操作,从而在不牺牲代码简洁性和可读性的前提下,提高程序的性能和效率。

完美转发

引用坍缩规则: 无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。

因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。

完美转发就是基于上述规律产生的。
所谓完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)。 为了解决这个问题,C++11使用 std::forward 来进行参数的转发(传递):

#include 
#include 
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 传参: ";
    reference(std::static_cast<T>(v));
}
int main() {
    std::cout << "传递右值:" << std::endl;
    pass(1);
    std::cout << "传递左值:" << std::endl;
    int v = 1;
    pass(v);
    return 0;
}

输出结果为:

传递右值:
              普通传参: 左值引用
       std::move 传参: 右值引用
    std::forward 传参: 右值引用
static_cast 传参: 右值引用
传递左值:
              普通传参: 左值引用
       std::move 传参: 右值引用
    std::forward 传参: 左值引用
static_cast 传参: 左值引用

无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发(一个声明的右值引用其实是一个左值), 所以 std::move 总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。

唯独 std::forward 即没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数。

std::forward 和 std::move 一样,没有做任何事情,std::move 单纯的将左值转化为右值, std::forward 也只是单纯的将参数做了一个类型的转换,从现象上来看, std::forward(v) 和 static_cast(v) 是完全一样的

你可能感兴趣的:(c++基础语法,c++)