初探值与引用:学会正确地使用右值引用
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 |