• C++右值引用、万能引用、完美转发和引用折叠


    目录

    什么是左值,什么是右值?

    右值引用

    万能引用

    引用折叠

    完美转发


    什么是左值,什么是右值?

    (接下来我们将左值称为 lvalue,右值成为 rvalue)

    左值通常指的是变量,或者说是可以放到等号左边的表达式。右值通常是常量表达式或者函数返回值(临时对象)。

    更加精简的说法是:

    如果可以对一个表达式取地址,那这个表达式就是lvalue。

    其他情况下,这个表达式就是一个rvalue。

    举例子:

    常见的左值的情况

    1. int x=2,y=3;//x,y就为左值
    2. ++x;--y;//表达式为左值
    3. const int& z = x;//z也为左值

    右值又可以分为两种情况:纯右值将亡值

    纯右值:基本类型(int,char等)的常量或者临时对象。

    1. 100,true//常量
    2. x++,x+1//表达式

    将亡值:自定义类型的临时对象 和 函数返回对象类型的右值引用

    1. int&& fuc1(void)
    2. {
    3. return 100;
    4. }
    5. string fuc2(void)
    6. {
    7. string str = "hello";
    8. return str;
    9. }
    10. int main()
    11. {
    12. fuc1();//函数返回值为右值引用
    13. fuc2();//函数返回值为临时对象
    14. }
    15. //对对象类型右值引用的转换
    16. void fuc3()
    17. {
    18. static_cast<int&&>(100);
    19. std::move(100);
    20. }

    表达式的左右值与类型无关!

    这里有两个概念,一个是值类别,一个是值类型

    值类别:对应着左值和右值的概念

    值类型:数据类型。

    例如:

    int x = 100;

    const int& y = x;

    int&& z = 100;

    此时x是的值类别为左值,值类型为int类型。

    同理,y的值类别为左值,而值类型为const int&

    注意:z的值类别也是左值,值类型为 int&&。

    右值引用

    正常的左值引用是无法引用右值的(常左值引用可以),所以需要右值引用去引用右值。

    1. int main()
    2. {
    3. int x = 1,y = 2;
    4. //左值引用
    5. int a = 0;
    6. int& b = a;
    7. //左值引用不能引用右值,const左值引用可以,因为临时变量具有常性
    8. const int& e = 10;
    9. const int& f = x + y;
    10. //右值引用
    11. int&& c = 10;
    12. int&& d = x + y;
    13. //右值引用不能引用左值,但是可以引用move后的左值
    14. int&& m = move(a);
    15. return 0;
    16. }

    万能引用

    万能意思就和表面的意思一样,即既可以引用左值,也可以引用右值

    万能引用的形式也是“&&”,所以说如何区分一个引用是右值引用还是万能引用?

    总共有两种情况会出现万能引用

    1、函数模板参数

    template

    void fuc(T&& param); //此时为万能引用

    2、auto声明

    auto&& var2 = var1;//var2是一个万能引用

    这两种存在共同的特点都存在类型的推导。其实这两者可以归为一类,auto声明的变量的类型推导规则本质上和模板是一样的,所以使用auto的时候你也可能得到一个万能引用。

    例如:

    1. template<typename T>
    2. void fuc1(T&& param); //此时为万能引用
    3. void fuc2(int&& param);//没有类型推导
    4. int main()
    5. {
    6. int x = 100;
    7. fuc1(100);
    8. fuc1(x);
    9. return 0;
    10. }

    因为通用引用是引用,所以必须初始化。通用引用的初始化决定了它表示的是右值引用还是左值引用。fuc1(100)中100是右值,说明通用引用被一个右值初始化。fuc1(x)中x为左值,说明通用引用被一个左值初始化。

    有几种情况不要弄混淆

    1、引用必须得精确!

    即一定得是T&&的形式

    template
    void fuc(std::vector&& param);

    上面这个例子不具备T&& param的格式,而是vector&& param的格式,所以只是一个普通的右值引用。

    1. template<typename T>
    2. void fuc(vector&& param); //此时不是万能引用
    3. int main
    4. {
    5. vector<int> v;//左值
    6. fuc(v);//error...
    7. fuc(vector<int>());//vector()临时变量,右值
    8. return 0;
    9. }

    注意:const T&&也会使得通用引用失效。

    template
    void fuc(const T&& param);//此时为右值引用

    2、模板内部的函数参数为T&&的形式,注意分辨清楚!

    利用vector中的 push_back和emplace_back为例来说明这个情况。

    1. template <class T, class Allocator = allocator >
    2. class vector {
    3. public:
    4. ...
    5. void push_back(T&& x); // fully specified parameter type ⇒ no type deduction;
    6. ... // && ≡ rvalue reference
    7. };

    注意这种情况,虽然T&&是模板参数,但是此时这个参数是右值引用,原因是vector这个类就已经知道T是什么类型了,此时对于void push_back(T&& x);中的T没必要再推导了,缺少万能应用推导的那一个环节。也就是说push_back依赖于vector的实例化而实例化的类型就完全决定了push_back的函数声明。可以在看一下void push_back(T&& x);在类外面是如何定义的。

    1. template <class T>
    2. void vector::push_back(T&& x);//依赖于vector

    vector模板实例化的过程:

    以vector vs;为例

    1. template <class string, class Allocator = allocator >
    2. class vector {
    3. public:
    4. ...
    5. void push_back(string&& x); // && ≡ rvalue reference
    6. ...
    7. };

    push_back并没有用到类型推导,直接取决于vector的实例化。

    所以这也是为什么push_back有两个版本的原因。

    与此相反,vector中另一个插入的函数emplace_back就可以实现类型的推导。

    1. template <class T, class Allocator = allocator >
    2. class vector {
    3. public:
    4. ...
    5. template <class... _Valty>
    6. void emplace_back(_Valty&&... _val); // deduced parameter types ⇒ type deduction;
    7. ... // && ≡ universal references
    8. };

    类型 _Valty 是独立于模板参数 T 的,所以每次调用emplace_back时,_val就需要推导一次,这就是它比较巧妙的地方。

    1. template<class... Args>
    2. void std::vector::emplace_back(Args&&... args);

    看一下这两个的原码(msvc)

    1. template<class... _Valty>
    2. decltype(auto) emplace_back(_Valty&&... _Val)
    3. { // insert by perfectly forwarding into element at end, provide strong guarantee
    4. if (_Has_unused_capacity())
    5. {
    6. return (_Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...));
    7. }
    8. _Ty& _Result = *_Emplace_reallocate(this->_Mylast(), _STD forward<_Valty>(_Val)...);
    9. #if _HAS_CXX17
    10. return (_Result);
    11. #else /* ^^^ _HAS_CXX17 ^^^ // vvv !_HAS_CXX17 vvv */
    12. (void)_Result;
    13. #endif /* _HAS_CXX17 */
    14. }
    15. void push_back(const _Ty& _Val)
    16. { // insert element at end, provide strong guarantee
    17. emplace_back(_Val);
    18. }
    19. void push_back(_Ty&& _Val)
    20. { // insert by moving into element at end, provide strong guarantee
    21. emplace_back(_STD move(_Val));
    22. }

    可以看到在msvc下的push_back是基于emplace_back实现的。在这里,有两个比较重要的地方,一个是std::move std::forward两个函数,放在后面的完美转发再讲。


    引用折叠

    在这里,其实是涉及到类型的推导,也就是去推模板参数中的 T 具体是什么类型,而且是针对通用引用场景下的类型的推导。

    在通用引用下类型推倒的机制很简单:当实参是左值时,T的类型为左值引用,当传右值时,T被推导为非引用类型。

    例如:

    1. template<typename T>
    2. void fuc(T&& param); //此时为万能引用
    3. string str = "hello";
    4. fuc(str);//T推导为 string&
    5. fuc(string());//T推导为string

    str 为左值,所以T的类型会被推到为string&,那么param的类型就为 string& &&我们知道在C++里面引用的引用是非法的,例如int x = 100;  auto& &y = x;

    所以 string& &&这该如何解释呢?

    引用折叠!

    程序员是不能自己声明引用的引用,但是编译器可以在特定的上下文中生成,模板的实例化就是其中之一(另外还有三种情况),编译器生成的引用的引用就会触发引用折叠。

    引用折叠的情况

    由于引用有两种类型:左值引用&和右值引用&&,所以两两组合总共四种情况,分别是lvalue reference to lvalue reference,  lvalue reference to rvalue reference,  rvalue reference to lvalue reference, 以及 rvalue reference to rvalue reference。

    引用折叠的规则:

    如果两个引用中有任何一个是左值引用lvalue reference ,那么最终的结果一定是左值引用,否则就为右值引用。

    所以上面的string& &&最终得到param的类型为string&。

    1. template<typename T>
    2. void fuc(T&& param); //此时为万能引用
    3. int main()
    4. {
    5. int x = 100;
    6. int& lx = x;
    7. int&& rx = 100;
    8. fuc(x);//T被推导为int&
    9. fuc(lx);//T被推导为int&
    10. fuc(rx);//T被推导为int&
    11. return 0;
    12. }

    有些人会在上面的例子存在疑惑,这里的右值引用传进去为什么还是int&,其实在文章开头的时候就已经说过了,要区分值类别值类型。虽然x,lx,rx的值类型都不相同,分别为int,int&,int&&,但是他们的值类别是相同的,都是lvalue。根据上面的规则左值对应的就是类型的引用。

    除此之外,还有三种情况可能导致引用折叠

    auto&&

    auto本身类型推导与模板类型推导基本相同

    1. string str = "hello";
    2. auto&& lstr = str;//等价为 string& && lstr = str;
    3. auto&& rstr = string();//等价为 string&& lstr = string();

    typedef

    如下例子可以说明typedef场景也可能存在引用折叠的情况。 

    1. template<typename T>
    2. class myclass
    3. {
    4. typedef T&& RvalueRfeType;
    5. };
    6. int main()
    7. {
    8. myclass<int&> mc;
    9. }

    myclass mc;此时将int& 带入模板可以得到

    typedef int& && RvalueRfeType; 可以推出RvalueRfeType为一个左值引用 int&,所以并不是想象当中的右值引用,需要注意。

    decltype

    这里先留个坑,等到下次详细讲auto 与decltype时在回过来填坑。


    完美转发

    先看个例子

    1. void fuc(int& param)//左值引用
    2. {
    3. cout << "lvalue reference" << endl;
    4. }
    5. void fuc(int&& param)//右值引用
    6. {
    7. cout << "rvalue reference" << endl;
    8. }
    9. template<typename T>
    10. void PerfectFoward(T&& param)//通用引用
    11. {
    12. fuc(param);
    13. };
    14. int main()
    15. {
    16. PerfectFoward(100);//传一个右值
    17. return 0;
    18. }

    输出结果:lvalue reference

    传进来是个右值,为什么最终调用的却是fuc函数参数为左值引用 的版本呢?

    捋一捋过程:

    100是右值,模板类型的推导 T 为 int,然后param的类型就是 int&&,到这没问题,但是,忽略掉了此时的param为左值,那调用函数当然是去调用形参为左值引用版本的函数。

    所以说,在传参的过程中,右值引用在第二次传递参数的过程中,右值属性会发生丢失,导致调用的都是左值引用的函数。

    为了解决这个问题,引入完美转发

    1. template<typename T>
    2. void PerfectFoward(T&& param)
    3. {
    4. fuc(forward(param));//forward完美转发
    5. };

    forward的实现原理

    底层源码:

    1. template <typename T>
    2. T&& forward(typename std::remove_reference::type& param)
    3. {
    4. return static_cast(param);
    5. }

    在这里面有一个东西 remove_reference,这个就比较有意思了。他的作用是移除T中的引用部分。也就是将T&和T&&变成T

    1. template<typename _Tp>
    2. struct remove_reference
    3. { typedef _Tp type; };
    4. template<typename _Tp>
    5. struct remove_reference<_Tp&>
    6. { typedef _Tp type; };
    7. template<typename _Tp>
    8. struct remove_reference<_Tp&&>
    9. { typedef _Tp type; };

    这里其实也是模板推导的一些知识(不懂得可以去看一下effective modern c++里面的条款1),正因为如此才可以把T里面的引用给去除掉。

    (C++真的是语法让人又爱又恨,不得感叹有意思,但是就很麻烦)

    在头文件中有各种各样的模板完成这种需求的转换工作,把这些叫做模板元编程(TMP),抱歉我不是一个牛叉的C++程序员。我要走的路还很长。

    我们回过头来再来看看forward 的实现。

    假设我们现在给一个左值,int x = 100。

    1. template<typename T>
    2. void PerfectFoward(T&& param)
    3. {
    4. fuc(forward(param));//forward完美转发
    5. };
    6. int x = 100;
    7. PerfectFoward(x);

    此时T的类型为int&,并将int& 传递给forward。再来看forward这里

    1. int& && forward(typename std::remove_reference<int&>::type& param)
    2. {
    3. return static_cast<int& &&>(param);
    4. }

    remove_reference::type 会去除掉int& 中的&部分,所以返回值为int,那么引用折叠完之后最终得到的是:

    1. int& forward(int& param)
    2. {
    3. return static_cast<int&>(param);
    4. }

    static_cast返回值为一个左值引用,此时static_cast啥也没做,保留了输入参数的左值属性

    当传递一个右值的时候

    1. template<typename T>
    2. void PerfectFoward(T&& param)
    3. {
    4. fuc(forward(param));//forward完美转发
    5. };
    6. PerfectFoward(100);

     此时模板参数T被推导出为int,那么forward这里

    1. int&& forward(int& param)
    2. {
    3. return static_cast<int&&>(param);
    4. }

    此时static_cast的返回值为int&&,在这篇文章开头讲过,函数返回值的右值引用是一个右值,转换成功,将一个左值param转换为一个右值,保留了最开始输入参数的右值属性

    与forward对应的是move函数

    move函数就很直接,直接把任何类别的对象都转化为右值。

    move的源码

    1. template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept
    2. {
    3. return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
    4. }

    可以看到,不管怎样static_cast返回值都为右值。

    注意move是一个掠夺的机制,所以需要小心,针对右值是没什么问题的,但是针对左值时就需要小心,effective modern c++中建议,对右值使用 move,对通用引用使用forward,根据前面所描述的,想必你们也想到了在通用引用中用move可能会存在什么问题。

    讲了这么多,那么右值引用有什么用?

    我认为移动构造移动赋值

    1. class String
    2. {
    3. public:
    4. String(const char* str = " ")
    5. {
    6. _str = new char[strlen(str) + 1];
    7. strcpy(_str, str);
    8. }
    9. String(const String& s)//左值版本
    10. {
    11. cout << "String(const String& s)-深拷贝" << endl;
    12. _str = new char[strlen(s._str) + 1];
    13. strcpy(_str, s._str);
    14. }
    15. ~String()
    16. {
    17. delete[] _str;
    18. }
    19. private:
    20. char* _str;
    21. };
    22. String fuc(const char* str)
    23. {
    24. String tmp(str);
    25. return tmp;//返回的是临时对象
    26. }
    27. int main()
    28. {
    29. String s1("hello world");
    30. String s2(s1);//参数是左值
    31. String s3(fuc("临时对象-右值"));//临时对象,参数是右值-将亡值,用完就析构了
    32. return 0;
    33. }

     

    对于这个右值,而且是将亡值,没有必要进行深拷贝,用完直接析构,所以考虑移动拷贝

    1. String(String&& s)//右值,将亡值
    2. :_str(nullptr)//把空值交换,进行析构就没问题,随机值析构很危险
    3. {
    4. cout << "String(const String&& s)-移动拷贝" << endl;
    5. swap(_str, s._str);
    6. }

    直接将空间进行交换,这样效率高。

     

    参考的文章

    现代C++之万能引用、完美转发、引用折叠 - 知乎 (zhihu.com)

    《effective modern c++》

  • 相关阅读:
    会计制度设计名词解释大全
    【Day19】接口
    Hive-源码分析一条hql的执行过程
    java 常用包
    【微信小程序从入门到精通(项目实战)】——微电影小程序
    教育行业在用的云管平台是什么牌子?
    NVIDIA大模型平台软件全家桶开启云智能第二曲线
    Go语言语法分析之我想打同事的脸--编译
    JVM学习-字节码指令集(三)
    【Linux】进程间通信1-匿名管道1
  • 原文地址:https://blog.csdn.net/weixin_43164548/article/details/126033687