Under what conditions are std::forward<X> and static_cast<X&&> equivalent?

Viewed 96

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0644r1.html says

There are two ways that you can forward a variable: you can use std::forward or you can use static_cast directly (as this proposal's forwarding operator does):

   template <class X, class Y>
   decltype(auto) foo(X&& x, Y&& y) {
       return std::forward<X>(x)(std::forward<Y>(y));    // with std::forward
       return static_cast<X&&>(x)(static_cast<Y&&>(y));  // with static_cast, exactly equivalent
   }

Does the "exactly equivalent" apply because X and Y are template parameters and so X&& and Y&& are forwarding references? Or for some other reason?

I assume it doesn't always apply because if it did

  1. I would expect std::forward's documentation to say so, as std::move's does;
  2. There would be no reason for the 14% compilation speedup from replacing one with another.

My current best guess is that:

  1. std::forward doesn't compile in some cases static_cast does;
  2. But not vice versa: if std::forward compiles, so does static_cast;
  3. If it compiles, both have the same result.

But I am far from confident it is correct.

1 Answers

To be truly general, we have to look to the definitions of static_cast<X&&>(…) and std::forward<X>(…). Note that we may ignore the case where X is not a reference type, since via reference collapsing it ends up equivalent to the corresponding rvalue reference type. So let X be Y& or Z&&, where Y and Z are not reference types.

In the lvalue-reference case, the relevant static_cast capabilities are (where B is Y or a base class of it)

  1. B& → Y&
  2. Y &x(…);

and the instantiated declarations for std::forward are

constexpr Y& forward(Y&) noexcept;
constexpr Y& forward(Y&&) noexcept;  // definition ill-formed

In the rvalue-reference case, they are (where additionally D is Z or a class derived from it)

  1. B&& → Z&&
  2. D& → Z&&
  3. Z &&x(…);

and

constexpr Z&& forward(Z&) noexcept;
constexpr Z&& forward(Z&&) noexcept;

The reference parameters to std::forward are copy-initialized from the argument, whereas static_cast can do one of three things:

  1. direct-initialization from its operand, which causes overload resolution differences when explicit conversion functions are involved (as noted by Artyer),
  2. downcast, so that static_cast<Derived&&>(Base()) works while std::forward<Derived>(Base()) cannot implicitly convert the argument, or
  3. the implementation of std::forward itself, which (as one would hope!) doesn't contribute any distinction since it applies only to the rvalue case, where one of the two overloads of std::forward will equivalently accept any derived type.
Related