云计算百科
云计算领域专业知识百科平台

C++11 :右值引用的作用,引用折叠和完美转发

目录

一.右值引用的作用

1.1为什么要引入右值引用?

二.类型分类(了解)

三.引用折叠

四.完美转发


一.右值引用的作用

回顾左值引用的作用:

  • 充当别名,避免拷贝,直接绑定左值,操作同一块内存;
  • 允许修改原始对象;
  • 函数可以向外带出修改结果(输出型参数);
  • 作为返回值,使表达式成为左值,支持连续赋值(如 operator[]);
  • 1.1为什么要引入右值引用?

    通过重载区分左右值,针对即将销毁的右值实现移动语义,转移资源而不拷贝数据,消除不必要的内存复制开销。

    比如:

    在这个例子中传值返回需要创建临时对象,然后再拷贝到外面,这会造成两次构造,导致资源浪费,那么就有同学说了,现在编译器都优化了,构造减少了,资源浪费就少了,还有必要用到右值引用吗?我们先了解为什么编译器要优化

    传统只有左值引用传值传参中为了避免造成资源,通常用下面这种方法来写:

    形参用引用来接收,改变形参的同时就可以改变外面元素了,但是这里缺少可读性,所以编译器要进行优化来解决该问题

    再讲之前我们先来了解一下什么是拷贝构造,拷贝赋值,移动构造和移动赋值

    函数函数签名时机数据源类型核心行为
    拷贝构造 string(const string& s) 创建新对象(初始化阶段) 左值 /const 左值 深拷贝,新建内存复制字符
    移动构造 string(string&& s) 创建新对象(初始化阶段) 右值(临时、std::move) 资源转移,swap,不拷贝数据
    拷贝赋值 string& operator=(const string& s) 对象已存在,赋值覆盖 左值 /const 左值 释放自身旧资源,深拷贝新数据
    移动赋值 string& operator=(string&& s) 对象已存在,赋值覆盖 右值(临时、std::move) swap 转移资源,无数据拷贝

    总结:拷贝构造和拷贝赋值需要消耗资源,移动构造和移动赋值不需要消耗资源

    给出下列代码是为了和库里做区分,可帮助我们看出调用了那些构造

    namespace Jam
    {
    class string
    {
    public:
    typedef char* iterator;
    typedef const char* const_iterator;

    iterator begin()
    {
    return _str;
    }
    iterator end()
    {
    return _str + _size;
    }

    const_iterator begin() const
    {
    return _str;
    }

    const_iterator end() const
    {
    return _str + _size;
    }

    string(const char* str = "")
    :_size(strlen(str))
    , _capacity(_size)
    {
    //cout << "string(char* str)-构造" << endl;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
    }

    void swap(string& s)
    {
    ::swap(_str, s._str);
    ::swap(_size, s._size);
    ::swap(_capacity, s._capacity);
    }

    // 拷贝构造
    string(const string& s)
    {
    //cout << "string(const string& s) — 拷贝构造" << endl;

    reserve(s._capacity);
    for (auto ch : s)
    {
    push_back(ch);
    }
    }

    // 移动构造
    string(string&& s)
    {
    //cout << "string(string&& s) — 移动构造" << endl;
    swap(s);
    }

    string& operator=(const string& s)
    {
    //cout << "string& operator=(const string& s) — 拷贝赋值" << endl;
    if (this != &s)
    {
    _str[0] = '\\0';
    _size = 0;

    reserve(s._capacity);
    for (auto ch : s)
    {
    push_back(ch);
    }
    }

    return *this;
    }

    // 移动赋值
    string& operator=(string&& s)
    {
    //cout << "string& operator=(string&& s) — 移动赋值" << endl;
    swap(s);
    return *this;
    }

    ~string()
    {
    //cout << "~string() — 析构" << endl;
    delete[] _str;
    _str = nullptr;
    }

    char& operator[](size_t pos)
    {
    assert(pos < _size);
    return _str[pos];
    }

    void reserve(size_t n)
    {
    if (n > _capacity)
    {
    char* tmp = new char[n + 1];
    if (_str)
    {
    strcpy(tmp, _str);
    delete[] _str;
    }
    _str = tmp;
    _capacity = n;
    }
    }

    void push_back(char ch)
    {
    if (_size >= _capacity)
    {
    size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    reserve(newcapacity);
    }

    _str[_size] = ch;
    ++_size;
    _str[_size] = '\\0';
    }

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

    const char* c_str() const
    {
    return _str;
    }

    size_t size() const
    {
    return _size;
    }
    private:
    char* _str = nullptr;
    size_t _size = 0;
    size_t _capacity = 0;
    };

    // 传值返回需要拷贝
    string addStrings(string num1, string num2,string& str) {
    //string str;
    int end1 = num1.size() – 1, end2 = num2.size() – 1;
    // 进位
    int next = 0;
    while (end1 >= 0 || end2 >= 0)
    {
    int val1 = end1 >= 0 ? num1[end1–] – '0' : 0;
    int val2 = end2 >= 0 ? num2[end2–] – '0' : 0;
    int ret = val1 + val2 + next;
    next = ret / 10;
    ret = ret % 10;
    str += ('0' + ret);
    }
    if (next == 1)
    str += '1';
    reverse(str.begin(), str.end());

    return str;
    }
    }

    注:

    图一是在Linux上不带优化运行的结果,图二是一代编译器优化的结果,图三是二代编译器优化后的结果

    红色框:外部传入函数时需要创建临时对象调用构造然后进行拷贝构造,我们可以看出编译器优化后直接合二为一没有拷贝构造了

    绿色框:函数里面临时变量的构造

    蓝色框:传值返回时调用的是构造函数创建临时对象,然后再进行拷贝构造,后面可以看出不断被优化,甚至最后只创建了临时对象,里面的局部对象都没有创建

    右值引用有移动构造的场景:

    通过观察最后一张图,有人就会说,既然编译器都优化了,最后只剩构造,那还要移动构造干什么?

    1.不同编译器对优化也不统一,有些编译器优化,有些编译器不优化,所以还是有必要用到移动构造的

    2.下面的移动赋值可以体现出右值引用的价值

    注:

    这里我们可以看出虽然编译器有优化,但是还是会有拷贝赋值这种情况,我们来看一下有移动赋值的情况

    虽然这里最后也有移动赋值,但是相比较于拷贝赋值,移动赋值不消耗资源,所以还是有价值的

    深拷贝的自定义类型:如 vector/string/map…. 实现移动构造和移动赋值是有很大的价值的

    浅拷贝的自定义类型:如 Date/pair<int, int>….不需要实现移动构造和移动赋值。

    二.类型分类(了解)

    三.引用折叠

    出现场景:

    只有模板 / 万能引用(T&&)推导的时候才会发生引用折叠! 普通代码手动写 int& && 直接编译报错,不存在折叠。

    当类型推导产生引用叠加时,遵循:

    只要存在左值引用 &,结果最终就是左值引用 &; 只有两个都是右值引用 &&,结果才是右值引用 &&

  • T& & → T&
  • T& && → T&
  • T&& & → T&
  • T&& &&→ T&&
  • 口诀:左值引用传染,遇 & 得 &;全 && 才是 &&

    万能模板:

    template<class T>
    void Function(T&& t)
    {
    int a = 0;
    T x = a;
    //x++; // 部分场景下T带const,打开会报错
    }
    int main()
    {
    // 场景1:传入右值 10
    Function(10);
    // 10是纯右值 → T推导为 int
    // T&& = int&& → 实例化:void Function(int&& t)

    // 场景2:传入普通左值变量 a
    int a;
    Function(a);
    // a是左值 → T推导为 int&
    // T&& = int& && →【引用折叠】→ int&
    // 实例化:void Function(int& t)

    // 场景3:std::move(a) 亡值(右值)
    Function(std::move(a));
    // move(a)是右值 → T推导为 int
    // T&& = int&& → void Function(int&& t)

    // 场景4:const左值 const int b
    const int b = 8;
    Function(b);
    // const左值 → T推导为 const int&
    // T&& = const int& && → 折叠 → const int&
    // 实例化 void Function(const int& t)
    // T = const int&,所以 T x = a; 得到 const int& x;x不可修改,x++编译报错

    // 场景5:std::move(b) const右值
    Function(std::move(b));
    // move(const变量) 是const右值
    // T推导为 const int
    // T&& = const int&&
    // T = const int,T x = a → const int x;依然不能x++
    return 0;
    }

    万能引用推导规律(背诵)

    调用 Function(arg)

  • arg 是非 const 左值 → T = 类型& → 折叠 → 参数 类型&(左值引用)
  • arg 是 const 左值 → T = const 类型& → 折叠 → 参数 const 类型&
  • arg 是右值(纯右值 /std::move 亡值) → T = 类型 → 参数 类型&&
  • arg 是 const 右值 → T = const 类型 → 参数 const 类型&&
  • 传左值,T 推导成引用类型;传右值,T 推导成普通值类型。

    四.完美转发

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C++11 :右值引用的作用,引用折叠和完美转发
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!