The Implementation of Std::Forward

The implementation of std::forward

The problem with the first is that you can write std::forward(x), which doesn't do what you want, since it always produces lvalue references.

The argument in the second case is a non-deduced context, preventing automatic deduction of the template argument. This forces you to write std::forward<T>(x), which is the right thing to do.

Also, the argument type for the second overload should be typename identity<T>::type& because the input to idiomatic use of std::forward is always an lvalue.

Edit: The standard actually mandates a signature equivalent to this one (which, incidentally, is exactly what libc++ has):

template <class T> T&& forward(typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward(typename remove_reference<T>::type&& t) noexcept;

Understanding of the implementation of std::forward since C++11

forward is essentially a machinery to conserve the value category in perfect forwarding.

Consider a simple function that attempts to call the f function transparently, respecting value category.

template <class T>
decltype(auto) g(T&& arg)
{
return f(arg);
}

Here, the problem is that the expression arg is always an lvalue regardless of whether arg is of rvalue reference type. This is where forward comes in handy:

template <class T>
decltype(auto) g(T&& arg)
{
return f(forward<T>(arg));
}

Consider a reference implementation of std::forward:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept
{
return static_cast<T&&>(t);
}

template <class T>
constexpr T&& forward(remove_reference_t<T>&& t) noexcept
{
static_assert(!std::is_lvalue_reference_v<T>);
return static_cast<T&&>(t);
}

(You can use decltype(auto) here, because the deduced type will always be T&&.)

In all the following cases, the first overload is called because the expression arg denotes a variable and hence is an lvalue:

  • If g is called with a non-const lvalue, then T is deduced as a non-const lvalue reference type. T&& is the same as T, and forward<T>(arg) is a non-const lvalue expression. Therefore, f is called with a non-const lvalue expression.

  • If g is called with a const lvalue, then T is deduced as a const lvalue reference type. T&& is the same as T, and forward<T>(arg) is a const lvalue expression. Therefore, f is called with a const lvalue expression.

  • If g is called with an rvalue, then T is deduced as a non-reference type. T&& is an rvalue reference type, and forward<T>(arg) is an rvalue expression. Therefore, f is called with an rvalue expression.

In all cases, the value category is respected.

The second overload is not used in normal perfect forwarding. See What is the purpose of std::forward()'s rvalue reference overload? for its usage.

std forward implementation and reference collapsing

Does it have to do with disallowing deducing types maybe?

Yes, typename std::remove_reference<T>::type introduces a non-deduced context. It prevents the user from mistakenly writing...

std::forward(something)

...and forces him/her to provide an explicit template argument:

std::forward<T>(something)

Difference between std::forward implementation

Try hsing it like std forward in a real context. Yours does not work;

void test(std::vector<int>&&){}

template<class T>
void foo(T&&t){
test(my_forward<T>(t));
}

foo( std::vector<int>{} );

The above does not compile. It does with std::forward.

Your forward does nothing useful other than block reference lifetime extension. Meanwhile, std::forward is a conditional std::move.

Everything with a name is an lvalue, but forward moves rvalue references with names.

Rvalue references with names are lvalues.

Is it true that std::forward and std::move do not generate code?

Whether something "generates code" or not depends on the compiler and its settings. As the other answer shows, you can expect some extra code to be generated if the optimizations are disabled.

std::move and std::forward merely return a reference to the parameter, which doens't require any actions at runtime (the change in value category happens at compile-time), and if optimizations are enabled, any half-decent compiler will generate no code for them.

If you want no extra code to be generated even in debug builds, use a static_cast<T &&> instead of those functions.

How does std::apply forward parameters without explicit std::forward?

You do not need to std::forward each element because std::get is overloaded for rvalue-reference and lvalue-reference of tuple.

std::forward<Tuple>(t) will give you either a lvalue (Tuple &) or an rvalue (Tuple &&), and depending on what you get, std::get will give you a T & (lvalue) or a T && (rvalue). See the various overload of std::get.


A bit of details about std::tuple and std::get -

As mentioned by StoryTeller, every member of a tuple is an lvalue, whether it has been constructed from an rvalue or a lvalue is of no relevance here:

double a{0.0};
auto t1 = std::make_tuple(int(), a);
auto t2 = std::make_tuple(int(), double());

The question is - Is the tuple an rvalue? If yes, you can move its member, if no, you have to do a copy, but std::get already take care of that by returning member with corresponding category.

decltype(auto) a1 = std::get<0>(t1);
decltype(auto) a2 = std::get<0>(std::move(t1));

static_assert(std::is_same<decltype(a1), int&>{}, "");
static_assert(std::is_same<decltype(a2), int&&>{}, "");

Back to a concrete example with std::forward:

template <typename Tuple>
void f(Tuple &&tuple) { // tuple is a forwarding reference
decltype(auto) a = std::get<0>(std::forward<Tuple>(tuple));
}

f(std::make_tuple(int())); // Call f<std::tuple<int>>(std::tuple<int>&&);
std::tuple<int> t1;
f(t1); // Call f<std::tuple<int>&>(std::tuple<int>&);

In the first call of f, the type of a will be int&& because tuple will be forwarded as a std::tuple<int>&&, while in the second case its type will be int& because tuple will be forwarded as a std::tuple<int>&.

Reason for using std::forward before indexing operator in Effective Modern C++ example

It depends on how the Container is implemented. If it has two reference-qualified operator[] for lvalue and rvalue like

T& operator[] (std::size_t) &; // used when called on lvalue
T operator[] (std::size_t) &&; // used when called on rvalue

Then

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i]; // call the 1st overload when lvalue passed; return type is T&
// call the 2nd overload when rvalue passed; return type is T
}

It might cause trouble without forwarding reference.

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
authenticateUser();
return c[i]; // always call the 1st overload; return type is T&
}

Then

const T& rt = authAndAccess(Container{1, 5, 9}, 0);
// dangerous; rt is dangling

BTW this doesn't work for std::vector because it doesn't have reference-qualified operator[] overloads.



Related Topics



Leave a reply



Submit