c++11新特性:右值引用

定义

是一种新的引用类型。可以来帮助解决unnecessary copying问题和实现完美转发。
当右手边是一个右值,那么左手边可以steal resources from right side,而不需要重新分配内存。

左值vs右值

  • 左值:可以出现在operator=左边的量。
  • 右值:只能出现在operator=右边的量。
    以int为例:
    1
    2
    3
    4
    5
    int a = 9;
    int b = 4;
    a = b;
    b = a;
    a + b = 42;//error a+b 是一个右值
    以string为例:
    1
    2
    3
    4
    5
    6
    string s1("Hello");
    string s2("world");
    s1 + s2 = s2; //ok
    cout << "s1:" << s1 << endl; //s1:Hello
    cout << "s2:" << s2 << endl; //s2:world
    string() = "World";
    以complex为例
    1
    2
    3
    complex<int> c1(3,8),c2(1,0);
    c1 + c2 = complex<int>(4,9);
    complex<int>() = complex<int>(4,9);
    从上面的例子来看,string和complex的临时对象可以出现在左边,这存在bug。
    但是临时对象一定是右值。
    在看一个例子:
    1
    2
    3
    4
    5
    6
    int foo(){
    return 0;
    }
    int x = foo();
    int *p = &foo(); //error 函数返回类型是右值
    foo() = 7; //右值不能被赋值danms

右值引用

如果需要减小临时对象释放和新对象内存分配和拷贝的开销,可以使用右值引用。
右值引用拷贝构造的底层原理就是对指针的浅拷贝。
需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

1
2
3
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

1
2
3
int && a = 10;
a = 100;
cout << a << endl;

引用折叠 & 万能引用

  • 引用折叠:C++中禁止reference to reference,所以编译器需要对四种情况(也就是L2L,L2R,R2L,R2R)进行处理,将他们“折叠”(也可说是“坍缩”)成一种单一的reference。
  • 万能引用:T&&
    • T && 碰到右值int &&, T匹配成int;
    • T && 遇到左值 int ,也能匹配,T此时是int &。
    • T && 碰到左值const int,T匹配为 const int &。
    • T &&碰到左值const int *(指针类型), T匹配为const int *&
    • T &&碰到左值const int * const(指针类型), T匹配为const int *const &
      例如:
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void testForward(T && v){}

int main(int argc, char * argv[])
{
testForward(1); // case 1
int x = 1;
testForward(x); // case 2
const int &rx = x;
testForward(rx); // case 2
}

万能引用把实参类型推导为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//case 1 
template<>
void testForward<int>(int && v)
{
}
//case 2
template<>
void testForward<int &>(int & v)
{
}
//case 3
template<>
void testForward<const int &>(const int & v)
{
}

完美转发

  • 动机:看下面这个例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    template<typename T>
    void print(T & t){
    std::cout << "Lvalue ref" << std::endl;
    }

    template<typename T>
    void print(T && t){
    std::cout << "Rvalue ref" << std::endl;
    }

    template<typename T>
    void testForward(T && v){
    print(v);//v此时已经是个左值了,永远调用左值版本的print
    print(std::forward<T>(v)); //本文的重点
    print(std::move(v)); //永远调用右值版本的print

    std::cout << "======================" << std::endl;
    }
    int main()
    {
    int x = 1;
    testForward(x); //实参为左值
    testForward(std::move(x)); //实参为右值
    }
    // Lvalue ref
    // Lvalue ref
    // Rvalue ref
    // ======================
    // Lvalue ref
    // Rvalue ref
    // Rvalue ref
    // ======================

用户希望testForward(x);最终调用的是左值版本的print,而testForward(std::move(x));最终调用的是右值版本的print。

可惜的是,在testForward中,虽然参数v是右值类型的,但此时v在内存中已经有了位置,所以v其实是个左值!(请仔细阅读这段话,保证你理解了)

所以,print(v)永远调用左值版本的print,与用户的本意不符。print(std::move(v));永远调用右值版本的print,与用户的本意也不符。只有print(std::forward(v));才符合用户的本意,这就是本文的主题。

不难发现,本质问题在于,左值右值在函数调用时,都转化成了左值(也就是有名称,可以取地址),使得函数转调用时无法判断左值和右值。

在STL中,随处可见这种问题。比如C++11引入的emplace_back,它接受左值也接受右值作为参数,接着,它转调用了空间配置器的construct函数,而construct又转调用了placement new,placement new根据参数是左值还是右值,决定调用拷贝构造函数还是移动构造函数。

std::forward源码

1
2
3
4
5
6
7
8
9
10
11
//接受左值
template<typename _Tp> constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept{
return static_cast<_Tp&&>(__t);
}
//接受右值
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
  • 当我们传入T = int &,经过模板参数推导和引用折叠:

    1
    2
    3
    constexpr int & && //折叠
    forward(typename std::remove_reference<int &>::type& __t) noexcept //remove_reference的作用与名字一致,不过多解释
    { return static_cast<int & &&>(__t); } //折叠

    最终转化为:

    1
    2
    3
    constexpr int & //折叠
    forward(int & __t) noexcept //remove_reference的作用与名字一致,不过多解释
    { return static_cast<int &>(__t); } //折叠
  • 当我们传入std::move(int)
    模板推导:

    1
    2
    3
    constexpr int && 
    forward(typename std::remove_reference<int>::type& __t) noexcept //remove_reference的作用与名字一致,不过多解释
    { return static_cast<int &&>(__t); }

    经过引用折叠:

    1
    2
    3
    constexpr int &&
    forward(int & __t) noexcept //remove_reference的作用与名字一致,不过多解释
    { return static_cast<int &&>(__t); }

    万能引用绑定到右值上时,不会发生引用折叠,所以这里没有引用折叠。