1

(constexpr and noexcept are left out, since they seem irrelevant for the purpose of understanding how std::forward behaves.)

Based on my understanding of Scott Meyers' "Effective Modern C++", a sample implementation of std::move in C++14 is the following

template<typename T>
decltype(auto) move(T&& param) {
  return static_cast<remove_reference_t<T>&&>(param);
}

Given the explanation of what a forwarding (or "universal") reference is, this implementation, I think, is pretty clear to me:

  • the parameter param is of type T&&, i.e. an rvalue reference or lvalue reference (to whichever the type of the argument is), depending on whether the argument is an rvalue or lvalue in the caller; in other words param can bind both to rvalues and lvalues (i.e. anything); this is intended, since move should cast anything to rvalue.
  • decltype(auto) is just the concise way to express the return type based on the actual return statement.
  • the returned object is the same object param, casted to an rvalue reference (&&) to whatever the type T is, once its deduced referenceness is stripped off (the deduction is done on T&&, not on ⋯<T>&&).

In short, my understanding of the use of forwarding/universal references in the implementation of move is the following:

  • the forwarding/universal reference T&& is used for the parameter since it is intended to bind to anything;
  • the return type is an rvalue reference, since move is intended to turn anything to rvalue.

It'd be nice to know if my understanding is right so far.

On the other hand, a sample implementation of std::forward in C++14 is the following

template<typename T>
T&& forward(remove_reference_t<T>& param) {
  return static_cast<T&&>(param);
}

My understanding is the following:

  • T&&, the return type, must be a forwarding/universal reference, since we want forward to return either by rvalue reference or by lvalue reference, hence type deduction takes place on the return type here (unlike what happens for move, where type deduction takes place on the parameter side) is an rvalue reference to whichever template type argument is passed to forward;
  • since T encodes the lvalue/rvalue-ness of the actual argument which binds the callers' parameter that is passed as argument to forward, T itself can result to be actual_type& or actual_type, hence T&& can be either an lvalue reference or rvalue reference.
  • The type of param is an lvalue reference to whatever the type T is, once its deduced referenceness is stripped off. Actually in std::forward type deduction is disabled on purpose, requiring that the template type argument be passed explicitly.

My doubts are the following.

  • The two instances of forward (two for each type on which is it called, actually) only differ for the return type (rvalue reference when an rvalue is passed, lvalue reference when an lvalue is passed), since in both cases param is of type lvalue reference to non-const reference-less T. Isn't the return type something which does not count in overload resolution? (Maybe I've used "overload" improperly, here.)
  • Since the type of param is non-const lvalue reference to reference-less T, and since an lvalue reference must be to-const in order to bind to an rvalue, how can param bind to an rvalue?

As a side question:

  • can decltype(auto) be used for the return type, as it is done for move?
Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 2
    `std::forward`'s second overload is `forward(remove_reference_t&& param)`, and `remove_reference` does not remove cv-qualifiers – Piotr Skotnicki Aug 03 '19 at 13:02
  • @PiotrSkotnicki, now I see that there are actually two overloads of [std::forward](https://en.cppreference.com/w/cpp/utility/forward). I wander why only the former is mentioned in Meyers' book... – Enlico Aug 03 '19 at 17:17
  • You forgot `constexpr` and `noexcept`. – Deduplicator Aug 03 '19 at 17:23
  • @Deduplicator, are this two guys necessary to understand how `std::forward` works? – Enlico Aug 03 '19 at 17:28
  • @EnricoMariaDeAngelis Only if you want to understand the parts they influence too. Or at least don't want to give a false impression, nor explain leaving them out. – Deduplicator Aug 03 '19 at 18:19

1 Answers1

1

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.

L. F.
  • 19,445
  • 8
  • 48
  • 82
  • It might be worth including (a reminder) that there are three different possibilities for `T`. (Also, since the question mentions it, the confusion about return types could be explained.) – Davis Herring Aug 04 '19 at 01:30
  • I've not accepted the answer yet since I've not understood yet (my bad). – Enlico Sep 18 '19 at 20:33
  • @EnricoMariaDeAngelis Maybe I can help you if you indicate precisely which part you don't understand. – L. F. Sep 19 '19 at 09:51
  • @L.F., I'm basically making confusion between the rvalue/lvalue-ness of the argument to `forward` and the rvalue/lvalue-ness of the argument to its caller. Indeed, regardless of `g`'s argument being an rvalue or an lvalue, the argument to `forward` is an lvalue (`arg`), therefore the second overload of 'forward` should never be called (not in the three examples you mentioned, at least, since in all of them `forward`'s argument has a name, `arg`), am I wrong? – Enlico Sep 19 '19 at 18:53
  • @EnricoMariaDeAngelis Exactly, the second overload is not used in perfect forwarding. – L. F. Sep 20 '19 at 06:19
  • @L.F., then I need a clarification about your third _If `g` is called with_, as there you say that _the second overload is selected_. – Enlico Sep 28 '19 at 10:21
  • @EnricoMariaDeAngelis My bad, I meant to say "`forward` is called with an rvalue" which is irrelevant. I've updated the answer, hopefully it's clearer – L. F. Sep 28 '19 at 12:32