引子
前文说到,std::move 源码中参数_Tp&&看起来像是个右值引用,但是在使用时却可以接收左值。
//std::move
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
int main(){
std::string a = "hello";
std::string&& b = std::move(a); //参数a是左值
}
实际上,这种情况下参数T&&是一个万能引用,或称做通用引用(Universal References)。 这块内容基本上只涉及到模板编程,但是为了能更好的啃下各种C++开源库,还是得了解下万能引用以及完美转发的概念。
万能引用与引用折叠
如果一个函数模板参数类型为T&&,其中T需要推导,那么T&&就是一个未定义的引用类型,称为万能引用,它既能绑定右值,又能绑定左值。 注意,只有当发生自动类型推断时(比如函数模板的类型自动推导,或者auto关键字),&&才是一个万能引用。 举个简单的例子,
template<typename T>
void func(T&& param){
}
int main(){
//例子1
func(1); //1是右值, param是右值引用
int a = 2;
func(a); //a是左值, param是左值引用
//例子2
std::string b = "hello";
auto&& c = b; //auto&&绑定左值
auto&& d = "world"; //auto&&绑定右值
}
例子中,T是一个模板,那么T就可能是int或int&或int&&,最后参数就可能变成(int&& && param)。 由于C++禁止reference to reference的情况,所以编译器会对L2L、L2R、R2L、R2R这四种引用做处理,折叠为单一引用,也就是引用折叠(Reference collapsing),具体就是:
- T& &、T&& &、T& &&都折叠成T&
- T&& &&折叠成T&& 这个比较好记,只要出现左值引用,都会最终折叠为左值引用。
完美转发
有了上面的概念之后,完美转发(Perfect Forwarding)这一块就很好理解了。
万能引用 + 引用折叠 + std::forward一起构成了完美转发的机制。
简单一点讲就是,std::forward会将输入的参数原封不动地传递到下一个函数中,如果是左值,传递到下一个函数还是左值,如果是右值,传递到下一个函数还是右值。
所谓perfect,指的就是不仅能准确地转发参数的值,还能保证其左右值属性不变。
为什么需要这个机制?
先看下面这段代码会输出什么?
template<typename T>
void func(T& param) {
cout << "传入左值" << endl;
}
template<typename T>
void func(T&& param) {
cout << "传入右值" << endl;
}
template<typename T>
void test(T&& t) { //参数t,万能引用
func(t);
}
int main() {
int a = 1;
test(a);
test(1);
}
输出是:
传入左值
传入左值
可以发现,无论传入左值右值,最终都调用了左值那个函数,和预期并不一致。
这是因为,无论调用test函数模板传递的是左值还是右值,对于函数内部的参数t来说,它有自己的名称,也可以获取地址,因此它永远都是左值。也就是说,传递给func函数的参数t一直是左值。(被声明的左值引用和右值引用本身就是一个左值,可以寻址)
上面这段话理解了之后,我们可以使用 std::forward 来改造下test函数,让它足够perfect。
template<typename T>
void test(T&& param) {
func(std::forward<T>(param));
}
到这里,应该理解C++出现完美转发的动机了。
在C++很多场景中,是否实现参数的完美转发,直接决定了这个参数的传递过程使用的是移动语义还是拷贝语义。
最后,我们再瞄一眼std::forward的函数定义:
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
前文已经说过,typename std::remove_reference<_Tp>::type 的作用是去掉参数_Tp的引用,只保留类型。
我们根据_Tp的引用类型分别简化下模板代码:
//情况1:接收左值,_Tp被推导为string&,那么_Tp&&就是string& &&,折叠为string&
string& forward(string& __t) {
return static_cast<string&>(__t);
}
//情况2:接收右值,_Tp被推导为string&&,那么_Tp&&就是string&& &&,折叠为string&&
string&& forward(string& __t) {
return static_cast<string&&>(__t);
}
总结
到这里,我们已经讲清楚了移动语义和完美转发,右值引用的作用就是支持这些机制。
说到底,这些新特性出现的意义是什么?C++11搞这么复杂,是不是多此一举?
这几个特性绝对不是可有可无的东西,它们的最终目的就是为了减少额外的拷贝开销,提升代码性能。但是也确实提升了理解成本,毕竟这几个特性,最算精通其他编程语言,光看源码也无法完全理解。不过只要理解了设计动机,一切解释起来就简单了。