完美转发

考虑一个简单的问题:在一个函数中要将值向下传递时,需要将原本的左右值关系依次传递下去,考虑下面的代码

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::forwardstatic_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 包了一层,在不考虑编译器优化的情况下多了一层调用,这也算是唯一的缺点吧。

最后修改:2021 年 11 月 28 日
如果觉得我的文章对你有用,请随意赞赏