0
点赞
收藏
分享

微信扫一扫

C++之右值引用与完美转发与可变参数模板(万字长文详解)

C++之右值引用与完美转发与可变参数模板

左值引用和右值引用的概念

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

那么到底什么是左值什么是右值呢?

难道说赋值符号左边的是左值,在赋值符号右边的就是右值吗?——错的!

左值引用和右值引用的比较

首先我们先思考一个问题既然左值引用能引用左值,右值引用能引用右值!那么——左值引用能不能引用右值?

==这是可以的!但是左值引用不能直接引用右值!要const的左值引用才能引用右值!==

int main()
{
       int a= 10;
       int& ra = a;
       //int& ra2 = 10;
       //这个是不行的!因为10是一个右值!

       //const引用既可以引用左值也可以引用右值!
       const int& ra3 = 10;
       const int& rr4 = a;
       //因为右值是一些不能修改的的值!所以我们可以用const引用来引用右值!
       //而且和权限有关系!右值就是就是权限平移
       //左值就是权限缩小!
       return 0;
}

==那么右值引用能不能引用左值呢?==——不可以!

int main()
{
       int a = 10;
       //int&& rra = a;
       //const int&& rra2 = a;
       //无论是不是const都无法引用左值!

       //但是有一个特例!
       int&& rra = std::move(a);//可以引用move后的左值!
       return 0;
}

==但是可以引用move后的左值!——后面我们会详细讲move有什么作用!==

右值引用的意义

那么右值引用究竟有什么意义呢?看上去好像没有什么用?

首先我要先想一想在以前我们使用引用(左值引用)的意义是什么?——在函数传参和函数返回的时候减少拷贝!

template<class T>
void Fun1(const T& x)//const左值引用的出现彻底解决了关于函数传参的问题!无论是左值还是右值都可以接收!
{
    cout << x <<endl;
}
int main()
{
     int x =10;
     Fun1<int>(10);//右值可以传
     Fun1<int>(x);//右值可以传
     return 0;
}

==但是左值引用并没有彻底解决传返回值的问题!==

template<class T>
const T& Fun1(const T& x)
{
       //......
       return x;
}
//如果出了作用域生命周期没有结束那么就可以用左值引用来返回!

template<class T>
const T& Fun2(const T& x)
{
       //....
       T ret = x;
       return ret;
}//如果是一个函数里面的变量!出了作用域就会被摧毁!那么就不能用左值引用来返回!
//左值引用无法解决这个问题!只能传值返回!
//虽然语法上允许但是不能使用!

==虽然我们上面的例子看上去没有什么,既然无法引用返回那么传值返回不就可以了么?看起来消耗也不大?——但是如果是下面这个例子呢?==

vector<vector<int>> generate(int numRows)
{
       vector<vector<int>> vv(numRows);
       for(int i = 0; i < numRows; ++i)
       {
           vv[i].resize(i + 1, 1);

       }
       for(int i = 2; i < numRows; ++i)
       {
           for(int j = 1; j < i; ++j)
           {
               vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
           }
       }
       return vv;
}

如果是int类型,那么确实拷贝代价不大!但是如果这是一个vector<vector< int >> 类型返回值呢?那么这个拷贝的代价可就太大了!

**在以前——是使用输出型参数来解决这个拷贝问题!**但是用起来不是那么的舒服!

==所以这就是右值引用的意义之一!——解决左值引用尚未解决的问题!==

那么右值引用是如何解决问题的呢?

//如果是找怎么写的!我们会发现!还是会报错!
template<class T>
T&& fun(const T& x)
{
       T ret = x;
       return x;
}
int main()
{
       int a = 10;
       int&& ra = fun(a);
       return 0;
}

image-20230509202203983

==那么为什么无法使用呢?首先我们得想明白一件事情——为什么我们不可以用左值引用?明明语法是支持了!因为出了作用域后变量就会被销毁!==

那么难道我们使用了右值引用后这个变量出了作用域就不会被销毁了吗?答案是不对的!==出了作用域这个变量依旧会被销毁!==——右值引用不是怎么使用的!

右值引用的原理与用法

在介绍右值引用的用法之前我们得先看是右值引用的原理是什么呢?

这是我们自己写的一个string类型

#include <iostream>
#include <algorithm>
#include<assert.h>
#include <cstring>

namespace MySTL
{
    class string
    {
        public:
        typedef char *iterator;

        iterator begin()
        {
            return _str;
        }

        iterator end() 
        {
            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);
            }

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

        // 拷贝构造
        string(const string &s)
            : _str(nullptr)
            {
                cout << "string(const string& s) -- 深拷贝" << endl;
                string tmp(s._str);
                swap(tmp);
            }


        string(string &&s)
            : _str(nullptr)
            {
                cout << "string( string&& s) " << endl;
                string tmp(s._str);
                swap(tmp);
            }

        // 赋值重载
        string &operator=(const string &s)
        {
            cout << "string& operator=(string s) -- 深拷贝" << endl;
            string tmp(s);
            swap(tmp);
            return *this;
        }


        ~string() 
        {
            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];
                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)
        string &operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }

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

        private:
        char *_str = nullptr;
        size_t _size = 0;
        size_t _capacity = 0; // 不包含最后做标识的\0
    };

    MySTL::string to_string(int value)
    {
        bool flag = true;
        if (value < 0) {
            flag = false;
            value = 0 - value;
        }
        MySTL::string str;
        while (value > 0) {
            int x = value % 10;
            value /= 10;
            str += ('0' + x);
        }
        if (flag == false) {
            str += '-';
        }
        reverse(str.begin(), str.end());
        return str;
    }
}
int main()
{
    MySTL::string ret = MySTL::to_string(1234);
    return 0;
}

==这样子的一个返回值会调用几次拷贝构造呢?——两次!(如果编译器不进行优化)==

image-20230509215633176

==那么为什么可以合二为一呢?——传值返回如果生成的临时对象比较小例如(4字节,8字节)那么就会被存在寄存器里面,如果比较大就会压在上一个的栈帧==

image-20230509220622196

==如果比较小就会放在寄存器,而不是栈帧!==

这是开启编译器优化后的结果

image-20230510115321507

==我们发现它甚至就是直接一个构造就结束了!==

这是关闭编译器优化的结果——如果你无法看到这个结果请手动关闭编译器优化!

image-20230510120316256

但是如果下面有的编译器可能会发生优化!

int main()
{
    MySTL::string ret;
    ret = MySTL::to_string(1234);
    return 0;
}

image-20230510120851075

==我这个编译器优化比较激进,一般来说除非是连续的构造!否则编译器是不敢进行优化的!==

MySTL::string ret;
int main()
{
    ret = MySTL::to_string(1234);
    return 0;
}

==但是怎么写!编译器就绝对不敢优化了——临时变量依旧存在==

image-20230510121019298

int main()
{
    MySTL::string ret;
    //或者在中间加一条代码编译器也不敢进行优化!
    for(int i = 0;i<1;++i);
    ret = MySTL::to_string(1234);
    return 0;
}

image-20230510121151809

==但是就算是编译器发生了优化!还是会有拷贝构造的发生!==

==还是没有彻底的解决问题!——如是一个大对象拷贝代价还是很大!==

==如果此时我们提供一个右值版本的拷贝构造!==

string(string &&s)
 :_str(nullptr) 
 {
       cout << "string(const string& s)" << endl;
       //.....
 }

此时给一个左值的对象就会去匹配左值引用版本的拷贝构造!

虽然右值可以匹配const的左值引用版本的拷贝构造!但是有了右值引用版本的的拷贝构造!就会因为更匹配而去匹配右值引用这个版本的拷贝构造!

==有了这个拷贝构造就我们就可以看到一下的现象==——如果读者无法看到该现象请手动关闭的你的编译器优化!

int main()
{
    MySTL::string s1("hello") ;
    MySTL::string s2(s1) ;
    MySTL::string s3(MySTL::string("world"));
    return 0;
}

image-20230510122042384

==在右值引用的拷贝构造的时候,s2调用拷贝构造!s3调用有右值引用的拷贝构造!匿名对象就是一个右值==

右值的分类

那么对于一个将亡值——我们是否有必要进行深拷贝呢?没有任何必要!

string(string &&s)//string&& s就是一个将亡值
	:_str(nullptr) 
{
  cout << "string(const string& s)" << endl;
  //.....
}

==既然这个对象就要被释放了!但是释放之前又要使用这个资源进行一次深拷贝!那么我们是不是有一个选择?==

==将这个将亡值的资源的所有权进行转移,给那个要进行深拷贝的对象!——那么不就既不用发生深拷贝,又构造出了对象了吗?==

string(string &&s)
    : _str(nullptr)
{
    cout << "string(const string& s)——移动构造" << endl;
    swap(s);//这样子我们就完成了所有权的转移!
}

==这种方式又叫做——移动拷贝!==

而之所以敢怎么做的原因就是因为有——右值引用的存在!有了右值引用后就可以韩浩的区分左值和右值了!——左值就只会去匹配深拷贝!而右值(将亡值)就回去匹配移动构造!不进行拷贝只进行资源权限的转移!

int main()
{
    MySTL::string s1("hello");
    MySTL::string s2(s1);
    MySTL::string s3(MySTL::string("world"));
    return 0;
}

image-20230510150610615

移动构造不要乱用!——因为移动构造本质是资源所有权的转移!

int main()
{
    MySTL::string s1("hello");
    MySTL::string s2(s1);
    //MySTL::string s3(MySTL::string("world"));
    MySTL::string s3(std::move(s1));//move可以将左值变成右值
    //这就是move的作用!
    return 0;
}

image-20230510151053691

namespace MySYL
{
       MySTL::string to_string(int value)
       {
           bool flag = true;
           if (value < 0) {
               flag = false;
               value = 0 - value;
           }
           MySTL::string str;
           while (value > 0) {
               int x = value % 10;
               value /= 10;
               str += ('0' + x);
           }
           if (flag == false) {
               str += '-';
           }
           reverse(str.begin(), str.end());
           return str;
       }
}
int main()
{
       MySTL::string ret = MySTL::to_string(1234);
       return 0;
}

这是我们上面写的一个代码在开启编译器优化后——是下面的这个结果!

image-20230510115321507

==其实本质就是编译器将左值给优化为了右值!==

image-20230510152058384

这样子传值返回的成本就很低了!

当然只有移动构造是不够了!因为还没有解决赋值的问题!

MySTL::string ret;
int main()
{
    ret = MySTL::to_string(1234);
    return 0;
}

image-20230510153744467

==所以我们还得补充一个移动赋值!==

string &operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    swap(s);//这样子这个值本身的资源还可以借用s进行释放!
    return *this;
}

有了移动赋值之后——我们就不用担心深拷贝的问题了!

image-20230510154057524

有了移动构造和移动赋值之后!我们就不用再担心传值返回了和赋值!——而这也才是==右值引用的真正用法!==

解决传值返回的是使用移动构造!而不是说直接将返回值改为右值返回!

解决负载是通过移动移动赋值!

==右值引用的都是通过间接的方式来解决==

有一个说法是——右值引用延长了生命周期其实是错误的!右值引用本质是将资源所有权给转移了!对象的声明周期是没有改变的!

有了移动构造和移动赋值就可以极大的提升了效率!

像是上面的这个例子只要vector提供了移动拷贝和移动赋值!那么我们就完全不用担心会出现多次的拷贝从而导致资源的浪费!

vector<vector<int>> generate(int numRows)
{
       vector<vector<int>> vv(numRows);
       for(int i = 0; i < numRows; ++i)
       {
           vv[i].resize(i + 1, 1);

       }
       for(int i = 2; i < numRows; ++i)
       {
           for(int j = 1; j < i; ++j)
           {
               vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
           }
       }
       return vv;//vv的这个返回值是一个将亡值是一个右值!
}

左值引用与右值引用的原理不同

左值引用和右值引用都是为了减少拷贝,但是原理不一样!

左值引用是取别名,直接起作用!

右值引用是间接起作用!是通过移动构造和移动赋值!——在拷贝的场景中,如果是右值(将亡值)那么就直接转移资源!

关于const右值引用和右值引用

为什么要有const右值引用呢?

左值引用可以修改,const左值引用不能修改!

但是==右值本身就是一个不可修改的值!所以为什么要有const右值引用呢?==

int main()
{
       double x =1.1,y = 2.2;
       int&& rr1 = 10;
       const double&&  rr2 = x+y;

       rr1++;//这个是可以的!
       //rr2++;//这个是不可以的!

}

==这是为什么?右不是不可以修改的吗?==

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

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

==右值不能取地址!但是右值被引用后可以认为变成了左值!==

int main()
{
       double x =1.1,y = 2.2;
       int&& rr1 = 10;
       const double&&  rr2 = x+y;

       //下面两个不行,不能取右值的地址!
       //cout << &(x+y) <<endl;
       //cout << &10 << endl;
       //但是可以取右值引用的地址!
       cout << &rr1 << endl;
       cout << &rr2 << endl;
}

image-20230511090628629

==像是rr1++,其实不是在给10进行++,而是在被存储在特定空间上的值进行++==

为什么啊要这样呢?——如果不这样我们==的移动构造就没有办法实现了!==

string(string &&s)
       : _str(nullptr)
       {
           cout << "string(const string& s)——移动构造" << endl;
           swap(s);//这个就是在修改右值引用!如果右值引用没有办法修改!那么就无法完成资源所有权的转移!
       }

当然了读者可能会怎么想!如何还是要开辟一个新的空间存不相当于还是进行了拷贝吗?像是10,存储在特定的空间中,不也是弄了一个int类型的变量进行存储吗?这样子看上去没有节省呀?

==但是如何是对于vector< vector< int >>这种类型的对象!我们在空间中只是存储了这个对象底层的几个指针!(或者用办法实现具体要看编译器的底层实现!)而不是真的将整个资源都存储在那个空间里面!进行所有权的转移本质就是指针的交换!==

STL容器的右值引用的运用

image-20230510163700292

右值引用在插入的运用

右值引用出了在返回值和赋值方面减少了拷贝之外!

对于一些右值数据的插入!也可以减少拷贝!

int main()
{
       list<MySTL::string> lt;
       MySTL::string str("111111");
       lt.push_back(str);
       lt.push_back(MySTL::string("111111"));
       lt.push_back("111111");
       //在没有构右值引用之前,这几种写法是没有什么区别的!
       return 0;
}

image-20230510165054735

万能引用的概念

模板中的&&

我们先看下面这个例子

void func1(int& t)
{
       cout << "void func1(int& t)" << endl;
}
void func2(int&& t)
{
       cout << "void func2(int&& t)" << endl;
}
int main()
{
       int x =1;
       func1(x);//匹配左值
       func2(1);//匹配右值
}

image-20230511150103921

int main()
{
       int x =1;
       func2(x);//但是右值不能匹配左值
       func2(1);//匹配右值
}

==但是上面只针对一般的函数!==

template<class T>
void perfectForward(T&& t)
{
}
int main()
{
       int x=1;
       perfectForward(1);
       perfectForward(x);
}

==但是我们发现如果是上面的模板函数那么就不会报错!==

image-20230511151446048

//这个我们一般叫做万能引用!——还有一个说法叫做引用折叠
template<class T>
void perfectForward(T&& t)
{
}

==如果传过来的是一个左值,那么这就是一个左值引用!如果传过来的是一个右值,那么这就是一个右值引用!==

万能引用的使用

int main()
{
       perfectForward(10);//右值

       int a = 10;
       perfectForward(a);//左值
       perfectForward(move(a));//右值

       const int b = 10;
       perfectForward(b);//const左值
       perfectForward(move(b));//const右值
}

==万能引用所有的引用都能接收!而且那么没有const也可以接受const引用!==

image-20230511153557296

==就是通过折叠来支持万能引用的!==

template<class T>
void perfectForward(T&& t)
{
       t++;//那么这个t能不能++呢?
}

==如果不上传const的左值和右值那么是可以++的!如果上传了const的左值和右值那么就不可以!因为本质就是实例化出了四个函数!前面两个函数可以支持++,后面两个实例化函数不支持++==

完美转发

通过上面完美引用的性质我们可以写出如下代码

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl;}
void Fun(const int &&x){ cout << "const 右值引用" <<endl; }
template<class T>
void perfectForward(T&& t)
{
       Fun(t);
}
int main()
{
       perfectForward(10);//右值

       int a = 10;
       perfectForward(a);//左值
       perfectForward(move(a));//右值

       const int b = 10;
       perfectForward(b);//const左值
       perfectForward(move(b));//const右值
}

那没事上面的代码会执行那几个Fun呢?

image-20230511154938888

==答案是都执行前两个去了!这是为什么呢?==

原因就出在这个t!

void perfectForward(T&& t)
{
       Fun(t);
}

如果t是一个左值就不用解释!肯定是调用前两个!

但是如果t是一个右值!我们上面说过!右值被引用后会被存储在一个特定的空间!——此时这个右值引用可以被修改,取地址!——==所以这个右值引用其实是一个左值!==

C++11新增默认成员函数

默认成员函数 原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数和移动赋值运算符重载。

默认移动构造

如果你**没有自己实现==移动构造函数==,且没有实现==析构函数== 、==拷贝构造==、==拷贝赋值重载==中的任 意一个。**那么编译器会自动生成一个默认移动构造。

默认移动赋值运算符重装

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。

default——强制生成默认函数

==假如我们真的遇到那非要写析构函数 、拷贝构造、拷贝赋值重载呢?——但是其实默认生成的移动构造就已经够用了!那么我们要怎么办呢?——我们可以使用defualt来强制生成!==

class Person
{
   public:
       Person(const char *name = "", int age = 0)
            : _name(name), _age(age)
           {
           }
       Person(const Person &p)
            : _name(p._name), _age(p._age)
           {
           }
   
       //我们可以看到这样子写其实很麻烦!
       /*Person(Person &&p)
           : _name(forward<MySTL::string>(p._name))//要使用move或者forward,否则p._name是左值,不能调用右值引用的构造函数
           , _age(p._age)
   {
   }*/
   
       //有了default我们就可以怎么写
       Person(Person &&p) = default;
       //这样子就可以生成一个默认移动构造!
       ~Person() 
       {
       }
   private:
       MySTL::string _name;
       int _age;
};
int main()
{
       Person s1;
       Person s2 = s1;//拷贝构造
       Person s3 = move(s1);//移动构造
       return 0;
}

image-20230512145654259

可变参数模板

C++中的可变参数模板对标的是c语言中的可变参数列表!

我们在最开始使用的printf就有用到可变参数列表!

image-20230514153248325

template<class ...Args>//可变参数模板!
void showlist(Args... args)
{
}
//class ...xxxx 就是用来声明模板参数包!
//Args是一个模板参数包!
//args是函数形参参数包!
//Args... args——这就是声明一个参数包!

C++中也延续了c语言的风格!用三个点来表示!

可变参数模板我们一般不会写!但是在库中经常可以看见!——像是thread的构造函数里面就使用了这个

emplace与一般插入的区别

image-20230514170704517

举报

相关推荐

0 条评论