初探值与引用:学会正确地使用右值引用
C++11中引入了右值引用,一起 look、look,这到底是个啥。
右值引用是右值?
引用,就是为了避免复制而存在,而左值引用和右值引用是为了不同的对象存在:
- 左值引用的对象是变量
- 右值引用的对象是常量
最直观的使用如下:
1 | int a =10; |
可能老铁理不清:右值引用和右值到底是啥关系?
先说结论:无论左值引用还是右值引用,都是左值,即上面的lr和rv是左值、是个变量,只是左值引用lr指向的是变量a,而右值引用rv指向常量10。
下面从demo中验证这一结论。假定有类Foo的实现如下:
1 | class Foo { |
以类Foo为基础,针对下面的代码进行案例分析:
1 | Foo foo(10); // 仅在此调用构造函数 |
(1)是最常用的左值引用,肯定不会发生复制行为(2)会报错:error: cannot bind rvalue reference of type ‘Foo&&’ to lvalue of type ‘Foo’。正是因为foo_rv_1是个右值引用,只能指向常量,而foo是个变量、是个左值。(2)报错是因为不能隐式地将左值转换为右值,但可以使用static_cast<Foo&&>强制转换,这是编译器所允许的。std::move函数底层即是如此实现,因此(3)和(4)等价。(5)会和(2)有同样的error。因为foo_rv_2本质上是个左值,不能将foo_rv_4引用foo_rv_2。(6)正确。因为foo_rv_2本身就是个左值,foo_lv_2去引用一个左值变量并没有问题。
因此上面的demo,只会在构造foo时调用一次构造函数。
此外,由于foo_lv_1、foo_lv_2及其foo_rv_2都指向foo,打印这三个变量地址会发现它们和foo的地址相同。说明不论左值引用还是右值引用都是左值这一事实。
1 | // 打印地址 |
注释:上面的demo中出于演示,foo在(3)中移动到foo_rv_2后,仍继续在(4)中使用。实际上不应该继续使用,除非给它重新初始化。
std::move
假定存在 construct_foo_by 函数,能根据传入的Foo对象的引用类型调用不同的构造函数构造Foo对象foo,那么应该如下设计:
当
construct_foo_by函数传入的对象是个右值时,construct_foo_by(Foo&& rhs)会被调用。但是!!! 由上面的分析可知,尽管rhs是右值引用类型Foo&&,但却是左值,想调用Foo的移动构造函数,必须强制将rhs变成右值。因此,在construct_foo_by函数内部,需要调用std::move函数来完成这一转换。注释:调用
construct_foo_by(Foo&& rhs)函数前,入口函数rhs的初始化相当于是Foo&& rhs = foo;,因此rhs也是个变量。当
construct_foo_by函数传入的对象是非右值时,rhs就是个左值,construct_foo_by函数内部自然就会调用Foo的拷贝构造函数创建foo。
整个demo实现如下:
1 | void construct_foo_by(Foo&& rhs) { |
因此,对于main函数中construct_foo_by的两次调用,输出如下
1 | $ g++ reference.cc -o ref && ./ref |
但是,如果将 construct_foo_by(Foo&& rhs)函数的实现修改为下面的版本,即内部不使用std::move函数对rhs进行转换:
1 | void construct_foo_by(Foo&& rhs) { |
输出如下,并不符合预期:本该调用Foo的移动构造函数来构造foo,却调用拷贝构造函数,就是因为rhs是左值,又没有经过std::move函数,最后触发了拷贝构造函数。
1 | $ g++ reference.cc -o ref && ./ref |
std::forward
在上面的实例中,知道了 右值引用是左值 这一事实,也正因为这个问题,导致 construct_foo_by(Foo&& rhs)函数内部再次调用std::move函数将rhs强制性转换为右值。
那么有没有办法可以使得不重载construct_foo_by函数,也依然能够根据传入参数的类型调用合适的构造函数?Of Course,答案就是std::forward函数。
但是std::forward必须配合模板使用,因为只有在模板参数下T&&才能触发引用折叠。T&&和具体的Foo&&不同,后者是具体类别的右值引用,而T&&可以是const Foo&、Foo&,也可以是Foo:
- 当输入的
rhs是左值类型时,T&&会被推断为Foo&,经过std::forward<T&&>强制转换后是Foo&&&,触发引用折叠后还是Foo&,最后调用拷贝构造函数; - 当输入的
rhs是右值类型时,T&&会被推断为Foo,经过std::forward<Foo&&>强制类型转换后变为Foo&&,触发移动构造函数。
注意:std::forward必须和模板搭配才能发挥完美转发的效果。
完整的实现如下:
1 | template<typename T> |
输出如下:
1 | $ g++ reference.cc -o ref && ./ref |