TIL: Perfect Forwarding and Universal Reference

While I was learning mtrebi's threadpool implementation, I noticed some peculiar usage of &&, which looked like this.

template<typename F, typename...Args>
auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {

&& is known for rvalue reference in C++, but what does it mean in this context? To answer this question, I did some research and figured it was called universal reference.

The term universal reference was coined by Scott Meyers, and it is used to describe references that may be either lvalue references or rvalue references may bind to anything. To understand it precisely, we need to understand what problem does it solve.

Perfect Forwarding Problem

void Bar(int& b) {
  printf("Bar called\n");
  b = 123;
}

Given function Bar, and we want to implement a wrapper function which allows use to pass some arguments to Bar via the wrapper function. We could do it as

template <typename T1>
void wrapper(T1& a) {
  Bar(a);
}

Then we could call it as

int k = 0;
wrapper(k);

It would work, but what if we want to call it as wrapper(0)? wrapper(0) should be valid because Bar(0) is a valid call. However, it would fail to compile as the compiler returns expects an l-value for 2nd argument.

To fix it, we could overload the wrapper function as

template <typename T1>
void wrapper(T1 a) {
  Bar(a);
}

However, it fixes the compiling error, but it changes the original meaning of Bar. We want to pass the reference of a variable to Bar, and this is pass by value.

Universal Reference

Before we give a solution to the previous problem, let's look at what universal reference brings to us.

Let's begin by looking at the problem that universal reference intends to solve.

In a context like this

template <typename T>
void wrapper(T&& a) {
}

&& becomes a universal reference which means it could be either a lvalue reference or rvalue reference based on the type of T, and we call this behaviour type deduction, and the rules that define the type deduction behaviour is called reference collapsing rules.

The rules are simple. If T is a lvalue, T&& becomes a lvalue reference, otherwise(rvalue) T&& becomes a rvalue reference.

By using universal reference, we could change our previous example to

template <typename T1>
void wrapper(T1&& a) {
  Bar(a);
}

When wrapper(k) applies, since k is a lvalue, the declaration of wrapper becomes wrapper(T1& a).

When wrapper(k) applies, since 0 is a rvalue, the declaration of wrapper becomes wrapper(T1&& a).

One Last Problem

Universal reference is great, it solves the wrong types of arguments problem we have before, however it is not perfect. Let's look at this code snippet.

void func(int& b) {
  printf("1\n");
}

void func(int&& b) {
  printf("2\n");
}

template <typename T1>
void wrapper(T1&& e1) {
    func(e1);
}

int main() {
  int k = 0;
  wrapper(0);
  return 0;
}

I expected the above example to output 2 as T1&& should be a rvalue reference as we passed a rvalue(0) to wrapper, however this was not the case. Surprisingly, 1 was the output, because once the argument 0 got passed into the function, it got a name which was e1, and it was no longer a rvalue as it got a name, and thus the lvalue reference version of func was invoked.

To fix this, what we have to do is cast it back to rvalue reference, but we need to be careful, we don't want to cast it always to rvalue reference, we want something like if T1 is rvalue reference, cast e1 to rvalue reference, otherwise cast it to lvalue reference. Fortunately, we can do it by using std::forward.

This is the definition of std::forward.

template<class T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
  return static_cast<T&&>(t);
}

template <class T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
  return static_cast<T&&>(t);
}

Let's apply both cases and see what std::forward returns.

When T1 is a lvalue reference, std::forward is

template<class T>
T1& && forward(typename std::remove_reference<T>::type& t) noexcept {
  return static_cast<T& &&>(t);
}

By applying the reference collapsing rules here, it is

template<class T>
T1& forward(typename std::remove_reference<T>::type& t) noexcept {
  return static_cast<T&>(t);
}

When T1 is a rvalue reference, std::forward is

template<class T>
T1&& && forward(typename std::remove_reference<T>::type& t) noexcept {
  return static_cast<T&& &&>(t);
}

which is equivalent to

template<class T>
T1&& forward(typename std::remove_reference<T>::type& t) noexcept {
  return static_cast<T&&>(t);
}

So the final solution for wrapper is

template <typename T1>
void wrapper(T1&& e1) {
    func(std::forward(e1));
}

Problem solved!

Function Ambiguity

This is something I noticed while I was studying this.

Consider this code snippet

void func(int& b) {
  printf("1");
}

void func(int b) {
  printf("2");
}

template <typename T1>
void wrapper(T1&& e1) {
    func(e1);
}

int main() {
  int k = 0;
  wrapper(k);
  return 0;
}

When we compile it, we see

perf.cpp:17:5: error: call to 'func' is ambiguous
    func(e1);
    ^~~~
perf.cpp:22:3: note: in instantiation of function template specialization
      'wrapper<int &>' requested here
  wrapper(k);
  ^
perf.cpp:7:6: note: candidate function
void func(int& b) {
     ^
perf.cpp:11:6: note: candidate function
void func(int b) {
     ^
1 error generated.

The reason for the compiling error is wrapper in this case is equivalent to

void wrapper(T& e1) {
    func(e1);
}

and both func(int& b) and function(int b) could be used, so the compiler fails to detect which is the correct function.

References

Perfect forwarding and universal references in C++, by Eli Bendersky

Answers to a stackoverflow question, by Kerrek SB