Forward Demystified
In this post I have shed some light on one of the darker corners of the C++ language: std::forward. The code snippets you will see are reduced parts of the main code at https://godbolt.org/z/dT3rPjPG3 . Also at the end, I have put the entire code and outputs from the terminal.
What is forward?
When working with templates, particularly when your template function calls another function and passes arguments provided to your template function to the inner function, it's crucial to ensure that rvalues are forwarded as rvalues and lvalues as lvalues to that inner function. This technique is known as perfect forwarding, and std::forward is the tool for achieving it.
Before diving deep into the inner workings of std::forward, there are three concepts that you should know.
1 - Reference Collapsing rules in C++
In C++ you can not have references to references the way you have pointers to pointers. For instance, code like this is meaningless and illegal:
int && & lref_to_rref;
But while working with types and templates these references will collapse.
A & to & will become &. A & to && or a && to & will also become a &. But && to && will become &&. For more info, you can take a look at the C++ Reference at https://en.cppreference.com/w/cpp/language/reference
The following explains that:
typedef int& lref;
typedef int&& rref;
int n;
lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&
2 - Forwarding References
Let's say you have a function with the following signature:
void func(int& var);
With that function, you can pass integer variables in but you can not pass values:
func(85) // won't compile
To be able to pass both variables and temporaries, you need to change the signature to:
void func(int&& var);
int&& is acting as a forwarding reference. It forwards both lvalues and rvalues to the function. Be aware that in the context of func, the var is itself an lvalue because now it has a name.
3 - What is std::move
It is just a cast. Essentially similar to the following:
template<typename T>
T&& move(T&& t)
{
return static_cast<typename std::remove_reference<Tp>::type&&>(t);
}
What is std::forward?
Now we are ready to see what std::forward does. The implementation for std::forward is very much like std::move but with one minor difference, the input argument is a & instead of &&:
template <class T> T &&forward(T &arg) {
return static_cast<T &&>(arg);
}
The Case Study
Imagine somewhere in our code we have a template function that can receive both lvalues and rvalues. In that function, we can construct a new object of T:
template <class T> void wrapper(T &&arg) {
T temp = arg;
}
In our main function, we invoke the wrapper function twice: once with a variable and the second time with a moved temporary
int main() {
Type var{};
wrapper(var);
std::cout << std::endl;
wrapper(std::move(var));
}
In our initial call, we pass var to the wrapper template function. Upon receiving a variable name, the wrapper template is left with no option but to treat its T as Type&, allowing it to accept Type& &&, which collapses to Type&.
So the arg is an lvalue reference to var and the temp expression in the wrapper becomes Type& temp = arg. temp is also an lvalue reference to var and no copies are made.
In our second call, we pass std::move(var) to the wrapper function. Since it is a moved value, and the wrapper already expects a T&&, the T will be deduced as Type without any &.
The issue is when we create the temporary. It will call the copy constructor of Type but we passed a moved value!
This is where std::forward comes into play. In our first call, it will create a reference to var, and in our second call, it will invoke the move constructor on Type, precisely as intended.
template <class T> void wrapper(T &&arg) {
T temp = std::forward<T>(arg);
}
But how?
This is where it can get really complicated. Pay attention to how we should call std::forward. We explicitly instantiate it with T as a template argument. You should always do that.
std::forward<T>(arg);
Considering the signature of std::forward, in our first call, std::forward is instantiated with T in wrapper = Type&, causing T in std::forward to become Type&. Consequently, std::forward will return Type& as well. So we will end up with temp as another lvalue ref to var in main.
In the second call, T in the wrapper function is Type, and when passed to std::forward<T>, T in std::forward becomes Type too. std::forward will return Type&&, effectively acting like std::move. Consequently, in this case, there will be a call to the move constructor of the temp.
This is how std::forward made a reference to var with no copying and called the move constructor when we moved from var in main.
The following is the complete code with many outputs that will help you track the changes in every call. Please share your thoughts on this.
#include <iostream>
#include <string_view>
template <typename T> constexpr auto type_name() {
std::string_view name, prefix, suffix;
#ifdef __clang__
name = __PRETTY_FUNCTION__;
prefix = "auto type_name() [T = ";
suffix = "]";
#elif defined(__GNUC__)
name = __PRETTY_FUNCTION__;
prefix = "constexpr auto type_name() [with T = ";
suffix = "]";
#elif defined(_MSC_VER)
name = __FUNCSIG__;
prefix = "auto __cdecl type_name<";
suffix = ">(void)";
#endif
name.remove_prefix(prefix.size());
name.remove_suffix(suffix.size());
return name;
}
class Type final {
public:
Type() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
Type(const Type &) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
Type(Type &&) noexcept { std::cout << __PRETTY_FUNCTION__ << std::endl; }
Type &operator=(const Type &) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
return *this;
}
Type &operator=(Type &&) noexcept {
std::cout << __PRETTY_FUNCTION__ << std::endl;
return *this;
}
~Type() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};
namespace {
int LineNumber{};
} // namespace
template <class T> T &&myForward(T &arg) {
std::cout << ++LineNumber << " " << __PRETTY_FUNCTION__ << std::endl;
std::cout << ++LineNumber << " " << type_name<decltype(arg)>() << std::endl;
return static_cast<T &&>(arg);
}
template <class T> void typeIndicator(T &&arg) {
std::cout << ++LineNumber << " " << __PRETTY_FUNCTION__ << std::endl;
std::cout << ++LineNumber << " " << type_name<decltype(arg)>() << std::endl;
}
template <class T> void wrapper(T &&arg) {
std::cout << ++LineNumber << " " << __PRETTY_FUNCTION__ << std::endl;
std::cout << ++LineNumber << " " << type_name<decltype(arg)>() << std::endl;
typeIndicator(myForward<T>(arg));
T temp = myForward<T>(arg);
}
int main() {
Type var{};
wrapper(var);
std::cout << std::endl;
wrapper(std::move(var));
}
Type::Type()
1 void wrapper(T &&) [T = Type &]
2 Type &
3 T &&myForward(T &) [T = Type &]
4 Type &
5 void typeIndicator(T &&) [T = Type &]
6 Type &
7 T &&myForward(T &) [T = Type &]
8 Type &
9 void wrapper(T &&) [T = Type]
10 Type &&
11 T &&myForward(T &) [T = Type]
12 Type &
13 void typeIndicator(T &&) [T = Type]
14 Type &&
15 T &&myForward(T &) [T = Type]
16 Type &
Type::Type(Type &&)
Type::~Type()
Type::~Type()
EV-Infrastructure Software Architect @ MAPNA (MECO)
9 个月Could you possibly explain this?