Forwarding in C++

Do these at first:

Reference Binding and Reference Collapsing

Move Semantics 中,我们提到,左值引用只能绑定左值,右值引用只能绑定右值。右值的应用让那些绑定右值的函数能够对物体直接进行所有权转移的操作,避免了不必要的拷贝。

然而,你会发现有时候当你传递左值给一个右值引用时,仍然行得通。但这应当是非法的。为什么呢?这是因为函数模板参数类型推导过程中会发生引用折叠。

#include <cstdio>
template<typename T>
void foo(T &&){
	printf("foo(T&&)");
}
int main(){
	int i = 5;
	foo(i); // i is a lvalue
	foo(5); // pass a rvalue
	return 0;
}

这里会发生 "reference collapsing"。我们传入了一个左值 i,在编译器推导函数 foo() 接收的类型时,会将其推导成 T& &&。而 C++ 有一些 reference collapsing rules 如下所示。进一步地,编译器会将类型推导成一个接受左值的函数 foo(T&)。(引用折叠,自 C++11)

Original Collapsed
& & &
&& & &
& && &
&& && &&

那传入一个右值会怎么样?传递右值给 foo 函数当然会推导出 T&&,从而保持右值引用的性质。也就是说,传递左值时,会折叠成左值引用;传递右值时,会保持右值引用。

Forwarding References (Perfect Forwarding w/ Template)

有时候,我们希望写出一个既能够接受左值,也能接受右值的函数。在这种情况下,C++11 引入了转发引用(forwarding references),也称为通用引用(universal references)。了解了引用折叠是怎么回事后,我们可以利用其特性编写一个中继函数来同时接受左值和右值。再通过中继函数对参数的转发,我们就实现了完美转发。

要实现完美转发,需要根据原始参数的值类别(左值/右值),保留其引用性质。这些都是在中继函数中完成的。我们需要用到 std::forward 保留原始的引用类别来传递给下层的函数。

template<typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
    return static_cast<T&&>(arg);
}
/*
std::move turns a arg into a rvalue
std::forward turns arg into a rvalue ref
*/

如:

#include <cstdio>
#include <utility>

template<typename T>
void foo(T& arg) {
    printf("lvalue foo\n");
}

template<typename T>
void foo(T&& arg) {
    printf("rvalue foo\n");
}

template<typename T>
void relay(T&& arg) {
    foo(std::forward<T>(arg)); // correctly forward the argument
}

int main() {
    int i = 5;
    relay(i); // i is a lvalue
    relay(5); // pass a rvalue
    return 0;
}

通过使用模板参数和 std::forward,我们实现了完美转发,从而在编写泛型代码时处理不同类型的引用。

Which to Forward?

如果我们有几个重载的引用转发,我们有下面的代码, f(w) 等调用这些重载的顺序是什么?

#include <utility>
struct Widget{};
// function with lvalue ref (1)
void f( Widget& ){}

// function with lvalue ref to const (2)
void f( const Widget& ){}

// function with rvalue ref (3)
void f( Widget&& ){}

// function with rvalue ref to const (4)
void f( const Widget&& ){};

// function with forwarding ref (5)
template< typename T >
void f( T&& ){}

// function template with rvalue ref to const (6)
template< typename T >
void f( const T&& ){}

int main(){
	Widget w{};
	f(w); // (1), (5), (2)

	const Widget w2{};
	f(w2); // (2), (5)

	f(std::move(w)); // (3), (5), (4), (6), (2)

	const Widget w3{std::move(w)};
	f(w3); // (4), (6), (5), (2)
	return 0;
}

Avoid overloading on forward references.