c++11新特性篇-右值引用

右值引用

左值和右值

C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。在介绍右值引用类型之前先要了解什么是左值和右值:

  • lvalue 是loactor value的缩写,rvalue 是 read value的缩写
  • 左值是指存储在内存中、有明确存储地址(可取地址)的数据;
  • 右值是指可以提供数据值的数据(不可取地址);

通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值所有有名字的变量或对象都是左值,而右值是匿名的

int a = 520;
int b = 1314;
a = b;

一般情况下,位于=前的表达式为左值,位于=后边的表达式为右值。也就是说例子中的a, b为左值,520,1314为右值。a=b是一种特殊情况,在这个表达式中a, b都是左值,因为变量b是可以被取地址的,不能视为右值。

将亡值和纯右值

在C++中,除了左值和右值的基本分类外,还有两个重要的概念:纯右值(pure rvalue)和将亡值(rvalue reference)。这两个概念在C++11之后变得尤为重要。

  1. 纯右值

纯右值是指那些临时对象、字面量、表达式的求值结果等,它们没有对应的内存位置(地址)可取纯右值通常是一些即将被销毁的临时值在C++中,通常将纯右值用于移动语义、临时对象的构造等操作

int getValue() {
    return 42;
}

int main() {
    int x = getValue();  // getValue() 是纯右值
    return 0;
}

在上面的例子中,getValue() 返回的临时整数是一个纯右值。

  1. 将亡值

将亡值是指能够被转移所有权的右值引用。在C++11之后引入的右值引用(Rvalue Reference)允许程序员声明一个引用,它只能绑定到右值。通过右值引用,可以实现移动语义,即将资源(如内存)的所有权从一个对象转移到另一个对象,而不进行深层的复制

int main() {
    int x = 42;
    int&& rvalueRef = std::move(x);  // std::move(x) 返回一个将亡值
    return 0;
}

在上面的例子中,std::move(x) 返回一个将亡值,然后通过右值引用 int&& 将其绑定到 rvalueRefstd::move 是一个用于将左值转换为右值引用的实用函数

总体而言,纯右值和将亡值这两个概念在C++中与移动语义、右值引用等一起使用,可以有效提高代码的性能和资源利用率。

右值引用

右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。**无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,**其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

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

#include 
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 getObj()
{
    return Test(); // 返回的是右值
}

int main()
{
    int a1;
    int &&a2 = a1;        // error
    Test& t = getObj();   // error (非常量引用的初始值不能为右值
    Test && t = getObj(); // 
    const Test& t = getObj();
    return 0;
}
  • 在上面的例子中int&& value = 520;里面520是纯右值,value是对字面量520这个右值的引用。
  • 在int &&a2 = a1;中a1虽然写在了=右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的
  • 在Test& t = getObj()这句代码中语法是错误的,右值不能给普通的左值引用赋值在用vs编译的时候会报错: 非常量引用的初始值不能为右值, 这里说的引用是左值引用, 不是右值引用
  • 在Test && t = getObj();中getObj()返回的临时对象被称之为将亡值,t是这个将亡值的右值引用。
  • const Test& t = getObj()这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值

应用场景

在C++中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。

右值引用的引入为移动语义提供了解决方案。通过移动构造函数和移动赋值运算符,可以将资源的所有权从一个对象转移到另一个对象,而不进行深拷贝。这种操作通常更为高效,因为它避免了不必要的复制,特别是对于临时对象(右值)而言。

代码示例:

#include 
#include 

class MyString {
public:
    // 普通构造函数
    MyString(const char* data) : size(std::strlen(data)), str(new char[size + 1]) {
        std::strcpy(str, data);
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept : size(other.size), str(other.str) {
        other.size = 0;
        other.str = nullptr;
    }

    // 移动赋值运算符
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] str;
            size = other.size;
            str = other.str;
            other.size = 0;
            other.str = nullptr;
        }
        return *this;
    }

    ~MyString() {
        delete[] str;
    }

private:
    size_t size;
    char* str;
};

int main() {
    MyString a = "Hello";  // 普通构造函数
    MyString b = std::move(a);  // 移动构造函数

    a = MyString("World");  // 移动构造函数 + 移动赋值运算符

    return 0;
}

在这个例子中,MyString 类具有普通构造函数、移动构造函数和移动赋值运算符。通过使用 std::move,我们可以将对象的所有权从一个实例转移到另一个实例,而不进行深拷贝。这种移动操作通过右值引用实现,大大提高了对象的资源管理效率。

代码运行结果:

普通构造函数
移动构造函数
普通构造函数
移动赋值运算

对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。

&& 的特性

在C++中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,在这两种场景下 &&被称作未定的引用类型。另外还有一点需要额外注意const T&&表示一个右值引用,不是未定引用类型

先来看第一个例子,在函数模板中使用&&:

template
void f(T&& param);
void f1(const T&& param);
f(10); 	
int x = 10;
f(x); 
f1(x);	// error, x是左值
f1(10); // ok, 10是右值

在上面的例子中函数模板进行了自动类型推导,需要通过传入的实参来确定参数param的实际类型。

  • 第4行中,对于f(10)来说传入的实参10是右值,因此T&&表示右值引用
  • 第6行中,对于f(x)来说传入的实参是x是左值,因此T&&表示左值引用
  • 第7行中,f1(x)的参数是const T&&不是未定引用类型,不需要推导,本身就表示一个右值引用

再来看第二个例子:

int main()
{
    int x = 520, y = 1314;
    auto&& v1 = x; //第4行
    auto&& v2 = 250;
    decltype(x)&& v3 = y;   // error
    cout << "v1: " << v1 << ", v2: " << v2 << endl;
    return 0;
};
  • 第4行中 auto&&表示一个整形的左值引用
  • 第5行中 auto&&表示一个整形的右值引用
  • 第6行中decltype(x)&&等价于int&&是一个右值引用不是未定引用类型,y是一个左值,不能使用左值初始化一个右值引用类型。把y换成一个常数就可以

由于上述代码中**存在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; // 7行
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为常量左值引用类型
  • 第15行:x为右值引用,不需要推导,只能通过右值初始化

也就是说, 只有纯右值能推导出右值引用, 剩下的都之能推导出左值引用

再看最后一个例子,代码如下:

#include 
using namespace std;

void printValue(int &i)
{
    cout << "l-value: " << i << endl;
}

void printValue(int &&i)
{
    cout << "r-value: " << i << endl;
}

void forward(int &&k)
{
    printValue(k);
}

int main()
{
    int i = 520;
    printValue(i);
    printValue(1314);
    forward(250);

    return 0;
};

测试代码输出的结果如下:

l-value: 520
r-value: 1314
l-value: 250

前两个没什么问题, 主要是最后一个: 首先250会作为参数传入forward函数, 相当于这句代码:

int&& k = 250;

也就是说k是一个右值引用, 再把一个右值引用作为函数参数进行传递, 那个函数能来接受呢?

答案是左值引用的那个函数可以接受, 也可以这么理解:

int&& b = 10;

int& a = b;
int&& a = b; //error

因为b已经是一个引用了, 不能再用右值引用来接收了, 右值引用应该接收一个纯右值.

所以上面代码示例中的k应该传递给打印l-value那个函数

总结:

  1. 左值和右值是独立于他们的类型的,右值引用类型可能是左值(引用折叠)也可能是右值。
  2. 编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值。
  3. auto&&或者函数参数类型自动推导的T&&是一个未定的引用类型,它可能是左值引用也可能是右值引用类型,这取决于初始化的值类型(上面有例子)。
  4. 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型,其余都是左值引用类型。

你可能感兴趣的:(c++,开发语言)