Given the following reference collapsing rules
T& &
--> T&
T&& &
--> T&
T& &&
--> T&
T&& &&
--> T&&
The third and fourth rule imply that T(ref qualifer) &&
is the identity transformation, i.e. T&
stays at T&
and T&&
stays at T&&
. Why do we have two overloads for std::forward
? Couldn't the following definition serve all purposes?
template <typename T, typename = std::enable_if_t<!std::is_const<T>::value>>
T&& forward(const typename std::remove_reference<T>::type& val) {
return static_cast<T&&>(const_cast<T&&>(val));
}
Here the only purpose the const std::remove_reference<T>&
serves is to not make copies. And the enable_if
helps ensure that the function is only called on non const values. I'm not entirely sure whether the const_cast
is needed since it's not the reference itself that's const.
Since forward
is always called with explicit template parameters there are two cases we need to consider:
forward<Type&>(val)
Here the type of T
in forward
will be T&
and therefore the return type will be the identity transformation to T&
forward<Type&&>(val)
Here the type of T
in forward
will be T&&
and therefore the return type will be the identity transformation to T&&
So then why do we need two overloads as described in http://en.cppreference.com/w/cpp/utility/forward?
Note: I am not sure if std::forward
is ever used with const
types, but I disabled forward
in that case, because I have never seen it used like that. Also move semantics don't really make sense in that case either.
The std::forward function as the std::move function aims at implementing move semantics in C++. The function takes a forwarding reference. According to the T template parameter, std::forward identifies whether an lvalue or an rvalue reference has been passed to it and returns a corresponding kind of reference.
std::move takes an object and casts it as an rvalue reference, which indicates that resources can be "stolen" from this object. std::forward has a single use-case: to cast a templated function parameter of type forwarding reference ( T&& ) to the value category ( lvalue or rvalue ) the caller used to pass it.
We use std::forward to retrieve the original value category. If for the handler we provided an rvalue then insert will move from it. If for the handler we provided an lvalue then insert will copy it. There are a lot of rules that come into play for the initial deceivingly simple code.
A good place to start would be Howard Hinnant's answer and paper on std::forward()
.
Your implementation handles all the normal use-cases correctly (T& --> T&
, T const& --> T const&
, and T&& --> T&&
). What it fails to handle are common and easy-to-make errors, errors which would be very difficult to debug in your implementation but fail to compile with std::forward()
.
Given these definitions:
struct Object { };
template <typename T, typename = std::enable_if_t<!std::is_const<T>::value>>
T&& my_forward(const typename std::remove_reference<T>::type& val) {
return static_cast<T&&>(const_cast<T&&>(val));
}
template <class T>
void foo(T&& ) { }
I can pass non-const
references to const
objects, both of the lvalue variety:
const Object o{};
foo(my_forward<Object&>(o)); // ok?? calls foo<Object&>
foo(std::forward<Object&>(o)); // error
and the rvalue variety:
const Object o{};
foo(my_forward<Object>(o)); // ok?? calls foo<Object>
foo(std::forward<Object>(o)); // error
I can pass lvalue references to rvalues:
foo(my_forward<Object&>(Object{})); // ok?? calls foo<Object&>
foo(std::forward<Object&>(Object{})); // error
The first two cases lead to potentially modifying objects that were intended to be const
(which could be UB if they were constructed const
), the last case is passing a dangling lvalue reference.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With