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