admin管理员组

文章数量:1442149

【C++11】右值引用 && 移动语义 && 完美转发

Ⅰ. 左值引用和右值引用

​ 传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。 无论左值引用还是右值引用,都是给对象取别名

一、什么是左值❓❓什么是左值引用❓❓

​ 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址或者可以对它赋值,左值可以出现在 = 的左边,右值不能出现在 = 表达式左边。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名

​ 简单的说,能取地址的就是左值!(虽然 C++11const 修饰的变量认为虽然不能修改值,但是它还是能修改地址的,所以 将常量视为左值

代码语言:javascript代码运行次数:0运行复制
int main()
{
    // 以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    
    // 以下几个是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;
    
    return 0; 
}	

二、什么是右值❓❓什么是右值引用❓❓

​ 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名

​ 简单点说,不能取地址的就是右值,且右值引用使用的符号是 &&

代码语言:javascript代码运行次数:0运行复制
int main()
{
    double x = 1.1, y = 2.2;
    
    // 以下几个都是常见的右值
    10; 
    x + y; 		// 因为表达式返回的值是临时的,所以是没办法取地址的
    fmin(x, y); // 函数调用也是没办法取地址的
    
    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    
    // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    10 = 1; 
    x + y = 1;
    fmin(x, y) = 1;
    
    return 0; 
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址

​ 举个例子,不能取字面量 10 的地址,但是通过 rr1 右值引用后,可以对 rr1 取地址,也可以修改 rr1。如果不想 rr1 被修改,可以用 const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

代码语言:javascript代码运行次数:0运行复制
int main()
{
     double x = 1.1, y = 2.2;
     int&& rr1 = 10;
     const double&& rr2 = x + y;
    
     rr1 = 20;
     rr2 = 5.5;  // const属性,修改会报错
    
     return 0; 
}

三、左右值总结

因此关于左值与右值的区分不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const 修饰的常量,不可修改,是只读类型的,理论应该按照右值对待,但因为其可以取地址,C++11 认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者临时对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。

总结:

  1. 不能简单地通过能否放在 = 左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如 常量也是左值
  2. 能得到引用的表达式一定能够作为引用,否则就用常引用。
  3. 能取地址的是左值,不能取地址的是右值

此外,C++11对右值进行了严格的区分:

  • 纯右值。比如:a+b100 等等。
  • 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。(后面讲移动构造的时候会讲到)

Ⅱ. 左值引用与右值引用比较

左值引用总结:

  1. const 左值引用只能引用左值,一般不能引用右值。
  2. const 左值引用既可引用左值,也可引用右值。
代码语言:javascript代码运行次数:0运行复制
int main()
{
    // 非const左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;     // ra为a的别名
    //int& ra2 = 10;  // 编译失败,因为10是右值
    
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    
    return 0; 
}

右值引用总结:

  1. 右值引用只能右值,一般不能引用左值。
  2. 使用 std::move() 可以将左值转化为右值进行引用。(这个下面会讲)
代码语言:javascript代码运行次数:0运行复制
int main()
{
    // 右值引用只能右值,不能引用左值。
    int&& r1 = 10;

    // error C2440: “初始化”: 无法从“int”转换为“int &&”
    // message : 无法将左值绑定到右值引用
    int a = 10;
    int&& r2 = a;  // ❌
    
    // 右值引用可以引用move以后的左值
    int&& r3 = std::move(a);
    
    return 0; 
}

Ⅲ. 右值引用的使用场景和意义

​ 问题:既然 C++98 中的 const 类型引用左值和右值都可以引用,那为什么 C++11 还要复杂的提出右值引用呢?下面我们来看看左值引用的短板,以及右值引用是如何补齐这个短板的!

代码语言:javascript代码运行次数:0运行复制
class string
{
public:
    string(const char* str = "")
        :_size(strlen(str))
        , _capacity(_size)
    {
        //cout << "string(char* str)" << endl;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }

    // 拷贝构造
    string(const string& s)
        :_str(nullptr)
    {
        string tmp(s._str);
        swap(tmp);
    }

    // 赋值重载
    string& operator=(const string& s)
    {
        string tmp(s);
        swap(tmp);
        return *this;
    }

    string operator+(char ch)
    {
        string tmp(*this);
        push_back(ch);
        return tmp;
    }

    ~string() { if (_str) delete[] _str;}
private:
    char* _str;
    size_t _size;
    size_t _capacity;
};
int main()
{
    string s1("hello");
    string s2("world");
    string s3(s1+s2);
    return 0; 
}

​ 上述代码看起来没有什么问题,但是有一个不太尽人意的地方:

​ 这是左值引用无法做到的一个短板,如果这里是重载 operator+=() 的话,那么返回的是 *this ,就可以使用左值引用进行返回。但这里重载的是 operator+()其返回的是一个临时对象,所以只能传值返回,传值返回会导致至少一次拷贝构造(如果是一些旧的编译器可能是两次拷贝构造),这大大的降低了程序的效率!

​ 所以就有了下面的右值引用!但是我们先了解一下移动语义:

Ⅳ. 移动语义/移动构造

C++11 中提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,而不是拷贝,这可以有效缓解效率问题!其实就是类似我们之前实现 operator=() 中,我们使用 swap 函数进行交换指针等操作的思想,只不过还要结合右值引用罢了!

​ 那么下面我们结合右值引用来解决这个问题:

​ 要注意的是,我们不是在 operator+() 上面进行 swap 操作,也不是直接将其返回值改为 string&& ,因为即使改成了这样子的话,当这个函数的作用域结束的时候,这个返回的右值引用其实也是和左值引用一样,是不存在的,那么这个时候就错误了,所以我们要换一种思路:移动构造函数

代码语言:javascript代码运行次数:0运行复制
// string的移动构造
string(string&& s)
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
{
	this->swap(s);  
}

// string的拷贝构造
string(const string& s)
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
{
    string tmp(s._str);
    this->swap(tmp);
}

​ 还记得上面我们介绍右值的时候,将右值分为两种:纯右值将亡值 (忘记的翻上去看)

​ 这里我们举的例子是关于 将亡值 的:

​ 不仅仅有移动构造,还有 移动赋值

代码语言:javascript代码运行次数:0运行复制
// 移动赋值(在类内实现)
string& operator=(string&& s) 
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    this->swap(s);
    return *this; 
}

int main()
{
    liren::string ret1;
    ret1 = liren::to_string(1234);
    return 0; 
}

// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

​ 这里运行后,我们看到调用了一次移动构造和一次移动赋值,这是因为如果是用一个已经存在的对象接收,编译器就没办法优化了(如果是在定义的时候就初始化,则原本需要两次的拷贝构造,因为编译器优化之后就只拷贝构造一次,这个下面讲编译器优化的时候会讲)。

liren::to_string 函数中会先构造生成一个临时对象,这里假设这个临时对象为 str,但是我们可以看到,编译器很聪明的在这里把 str 识别成了右值,调用移动构造。然后再把这个临时对象作为 liren::to_string 函数调用的返回值赋值给 ret1,这里调用的是移动赋值。

本文标签: C11右值引用 ampamp 移动语义 ampamp 完美转发