完美转发
考虑一个简单的问题:在一个函数中要将值向下传递时,需要将原本的左右值关系依次传递下去,考虑下面的代码
template<typename T>
void testReference(T& lref) {
printf("左值引用 %p\n", &lref);
}
template<typename T>
void testReference(T&& rref) {
printf("右值引用 %p\n", &rref);
}
template<typename T>
void pass(T&& ref) {
testReference(ref);
}
void test() {
pass("114");
pass(std::string("514"));
}
无论 pass
接收的是一个左值还是一个右值,ref
都会被作为左值被传递,因此最终输出会是2个左值,这显然与目的矛盾了。
STL 库中有一个模板函数 std::forward
可以用来解决这个问题:将 pass
中的 ref
修改为 std::forward<T>(ref)
即可实现完美转发。
template<typename T>
void pass(T&& ref) {
testReference(std::forward<T>(ref));
}
原理
引用折叠
在继续前,需要先了解 C++ 的引用折叠规则:
X& & => X&
X&& & => X&
X& && => X&
X&& && => X&&
因此,只有在传递给 ref
的是一个右值时,ref
的类型才会是 T&&
。但是 ref
本身是一个左值,因此直接传递会是调用左值引用版本。
库函数实现
clang 库中 std::forward
的实现如下:
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept {
return static_cast<_Tp&&>(__t);
}
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);
}
可以看到,关键点在于 std::remove_reference<_Tp>
,clang 库中它的实现是
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;
};
可以看出,它的作用是提供一个抹除类型 _Tp
的引用的类型 type
。
因此,typename std::remove_reference<_Tp>::type&
和 typename std::remove_reference<_Tp>::type&&
就分别代表了原始类型 T
的左值引用和右值引用,从而实现了当_Tp
为左值引用类型和右值引用类型时调用了不同的 forward
,并将 __t
转换为一个右值引用。不过,从实现上来看,它们做的实际上是同一件事:返回 static_cast<_Tp&&>(__t)
。
根据引用折叠规则,当 __t
是左值时,虽然将它 cast 成了右值引用,最终还是返回的一个左值;当 __t
是右值时,就会返回一个右值。
因此,现在我们来总结一下,std::forward<T>(ref)
基本等价于 static_cast<T&&>(ref)
,其原理都是引用折叠:
ref = T& => T&&(ref) = T&
ref = T&& => T&&(ref) = T&&
优点与局限
既然 std::forward<T>(ref)
等价于 static_cast<T&&>(ref)
,那为什么还需要它呢?考虑下面几种情况:
- 应该将左值作为左值转发
- 应该将右值作为右值转发
- 应该将派生类型的表达式转发到可访问的,明确的基本类型
- 不应转发任意类型的转换
其中(1)(2)是 std::forward
和 static_cast
都能做到的,而 (3)(4) 是 static_cast
不能做到的(虽然在现代编辑器下会有警告)。考虑下面的代码
struct Wrapper {
Wrapper(int);
};
template<class Arg1, class Arg2>
Arg1&& f(Arg1&& a1, Arg2&& a2) {
return static_cast<Arg1&&>(a2); // typing error: a1=>a2
}
template<class Arg1, class Arg2>
Arg1&& g(Arg1&& a1, Arg2&& a2) {
return std::forward<Arg1>(a2); // typing error: a1=>a2
}
其中第一个函数不会出现编译错误,而第二个会。因此使用 std::forward
更加安全。
不过,毕竟 std::forward
包了一层,在不考虑编译器优化的情况下多了一层调用,这也算是唯一的缺点吧。