引用折叠、万能引用、右值引用、move、完美转发

在写STL源码的时候遇到的问题,在这里写一篇笔记记录一下。

一、引用折叠

引用折叠表示了一个值被引用多次时(只有在模板推导时候),会生成什么类型。

T& & 折叠成 T& \
T& && 折叠成 T& \
T&& & 折叠成 T& \
T&& && 折叠成 T&&

其实总结起来就是,只有两次都是右值引用的时候才是T&&,即T&& && 折叠成 T&&,其他都是转换成左值引用T&。

二、左值引用

左值引用,就是个别名,将一个变量绑定到另一个变量上。

int a = 10;
int &b = a;     // 左值引用
const int &c = 10;  // 常量左值引用

b = 20;
std::cout << a << std::endl;  // 20

修改引用变量会影响原来的变量的值,但是 const int & 不能修改原对象的值(中间的int可以更换为其他类型)。\
一般来说,函数传参的时候是拷贝传参,也就是将变量复制到一个临时变量,再将临时变量传入函数,最后销毁临时变量。这里涉及到了内存的拷贝,消耗的时间比较长。但是如果在传参的时候使用左值引用,不会涉及到内存的拷贝,因为是别名,相当于直接修改原变量了。

void left_value(int& x) {
    x = 200;
}

int main()
{  
    int value = 10;
    left_value(value);
    std::cout << value << std::endl;  // 200
    return 0;
}

\
当然如果是const&的话,虽然不涉及到内存的拷贝过程,但是不能修改原变量。

void left_value(const int& x) {
    x = 200;     // error: assignment of read-only reference 'x'
}

int main()
{  
    int value = 10;
    left_value(value);
    std::cout << value << std::endl;  // 200
    return 0;
}

这里函数传参的时候,为什么 int 能匹配到 int& 或者 const int& 呢?

当你将一个int类型传递给int&参数时,c++会执行一个类型转换,将int隐式的转换为int&,另一个同理。注意:int、int&、const int& 在匹配函数的时候优先级一样,同时出现会发生歧义错误。 \
函数传参的时候隐式转换很常见。

void left_value(int num) {
    std::cout << num << std::endl;
}

int main()
{  
   float a = 1.5;
   left_value(a);    // 1
   return 0;
}

传入的时候隐式的将float类型转化为int类型。\

但是用函数重载就可以解决这个问题。

void left_value(int num) {
    std::cout << num << std::endl;
}

void left_value(float num) {
    std::cout << num << std::endl;
}

int main()
{  
   float a = 1.5;
   left_value(a);
   return 0;
}

会调用最适合的函数,那如果没有的话就只能进行隐式的类型转换了。

三、右值引用

右值引用是c++11引用的新特性。就是左值引用是给一个变量加别名,而右值引用就是绑定变量到一个临时值上,临时变量的生命周期和新左值的生命周期绑定,原来的临时变量销毁。

int &&d = 10;   // 右值引

d = 200;  // 此时 d 是变量是左值

引用在c++中是一个特别的类型,因为它的值类型和变量类型不一样, 左值/右值引用变量的值类型都是左值, 而不是左值引用或者右值引用。 这句话意思非常重要,简单来说,看上面代码,一开始将d绑定到了10上面,然后又将d赋值,在int &&d=10 之后,每次使用d,d都是作为一个左值使用的。一个右值引用变量在使用上就变成了左值,已经不再携带其是右引用这样的信息,只是一个左值。\

右值引用也是提高性能用的。

vector<string> v;
string s = "teststring";
v.push_back(s);

上面这个代码,用了一个临时变量。假如我们啥都不干那么是怎么进行的呢,首先会先创建一个临时字符串,然后将这个临时字符串拷贝到v的内存上,然后再销毁这个临时字符串。中间进行内存的开辟和销毁,在一般数据上性能没啥问题,如果是数据特别多的话,性能就会严重下降。\

但如果我们用右值引用,如下代码。

vector v;
string s = "teststring";
v.push_back(std::move(s));

这里的std::move()是将一个左值强制转换为右值,下面我会讲,现在就知道能将左值强制转化为右值就行了。\
中间的过程就变成了,创建一个临时字符串,然后直接将这个临时字符串挂载到v上面。是不是中间少了好多过程,性能也大大的提高了。

四、万能引用

模板编程中,有的时候需要传入左值也传入右值引用。\
下面这个只能传入一个左值,如果传入右值就会报错。

template <typename T>
void func(T& value) {
    std::cout << "函数调用" << std::endl;
}

int main()
{
    int value = 10;
    func(value);
    func(10); // 报错
    return 0;
}

\
这时候就用到了我们的万能引用T&&

template <typename T>
void func(T&& value) {
    std::cout << "函数调用" << std::endl;
}

int main()
{
    int value = 10;
    func(value);
    func(10); 

    return 0;
}

解释一下: \
能同时传入左值引用和右值引用。\
如果是左值,T先会推导成T&,然后再发生引用折叠,如果是右值引用,T会推导成T&&。\
比如说,func(value)中的value是一个左值,传入后T会推导成T&,然后再后后面的&&发生折叠引用,就会变成T&。\
func(10)中的10是一个右值,T会推导成T&&,然后再和后边的&&发生引用折叠,最后变成了T&&。
**T&&只在

五、move

std::move它其实啥都没干,就是强制将左值转化为右值。\
show you the code

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

这里面的 std::remove_reference<_Tp>,就像去除变量的引用,假如_Tp是int&&,最后返回type是个int。\

std::remove_reference<_Tp>的源代码如下。

/// remove_reference
  template<typename _Tp>
    struct remove_reference
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&>
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&&>
    { typedef _Tp   type; };

  template<typename _Tp, bool = __is_referenceable<_Tp>::value>
    struct __add_lvalue_reference_helper
    { typedef _Tp   type; };

  template<typename _Tp>
    struct __add_lvalue_reference_helper<_Tp, true>
    { typedef _Tp&   type; };

六、完美转发

完美转发是配合万能引用使用的,通过万能引用传入左值和右值的参数,然后通过完美转发保留这个属性,转发给对应的重载函数。\
show the code

/*
    函数模板的重载
*/
template <typename T>
void to_forward(T& value) {
    std::cout << "调用的左值函数" << std::endl;
}

template <typename T>
void to_forward(T&& value) {
    std::cout << "调用的右值函数" << std::endl;
}

template <typename T>
void func(T&& arg) {

    to_forward(arg);  // 调用左边值函数
}

int main(){
    int value = 10;
    func(std::move(value));
}

在这个代码中,func函数中的to_forward函数最终会调用to_forward(T& value),虽然传入的是右值,一个右值引用变量在使用上就变成了左值,已经不再携带其是右引用这样的信息,只是一个左值,所以这里调用的是左值的重载函数。\

首先展示一下std::forward的源码。

/**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }
 ```
 这里有两个重载的函数,一个对应左值一个对应右值。\
 ``forward(typename std::remove_reference<_Tp>::type& __t)``这个是先将_t的类型去掉引用,然后加上个&,也就是最后是左值类型。\
 那下面``forward(typename std::remove_reference<_Tp>::type&& __t)``那个肯定就是右值的了。\
 <br/>
 这里也用到了引用折叠,连个返回的方法是一样的,都是``return static_cast<_Tp&&>(__t)``,如果_Tp是&,然后在和后面的&&折叠就是左值了;如果_Tp是&&,然后再和后面的&&折叠还是&&。\
 <br/>
**std::move和std::forward其实什么都没做,只是强制转换了一下类型。** \
<br/>
看下面这个代码,这个是正确的。我们通过完美转发保留了这个左值还是右值的属性,然后再传给另一个函数。

```cpp
#include 
#include 

/* 完美转发 + 万能引用 + std::move */

/*
    函数模板的重载
*/
template 
void to_forward(T& value) {
    std::cout << "调用的左值函数" << std::endl;
}

template 
void to_forward(T&& value) {
    std::cout << "调用的右值函数" << std::endl;
}

/*
    万能引用:
        能同时传入左值引用和右值引用
        如果是左值,T先会推导成T&,然后再发生引用折叠
        入股是右值引用,则会什么都不干
    利用完美转发std::forward:
        先通过万能引用可以传入左值引用和右值引用
        然后通过完美转发(能保留传入时候是左值引用还是右值引用的属性,然后转发到对应的函数重载中
*/
template 
void func(T&& arg) {
    // 保留左值和右值的属性
    to_forward(std::forward(arg));
}

int main() {

    int value = 10;

    int& value_l_refernce = value;

    func(value);       // 调用的左值引用函数

    func(value_l_refernce);       // 调用的左值引用函数

    func(100);         // 调用的右值引用函数

    int&& value_r_refernce = 30;
    // 右值引用使用后,之后调用这个变量都是作为左值
    func(value_r_refernce);     // **调用的左值引用**

    func(std::move(value));     // 调用的右值引用

    return 0;
}
THE END