18

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:

  1. 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&
  2. 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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Curious
  • 20,870
  • 8
  • 61
  • 146
  • Becuase rvalue can't be bound to `val` if there's no the 2nd overload? – songyuanyao Jul 13 '16 at 06:47
  • @songyuanyao whoops, I had a const in there earlier, but somehow decided to remove it... – Curious Jul 13 '16 at 06:49
  • The example you give cannot bind to rvalues (see [this example](http://coliru.stacked-crooked.com/a/b0ae1eaf909513e0)) – Rerito Jul 13 '16 at 06:51
  • 4
    @Curious Using `const_cast` is dangerous. If the argument passed is a real `const` it leads to UB. – songyuanyao Jul 13 '16 at 06:53
  • @songyuanyao: But he disables the definition if it is `const`, so we can assume it is not const? I don't know why he has a `const_cast` there then, probably it can just be a `static_cast`. – Chris Beck Jul 13 '16 at 06:55
  • 1
    You may find Howard's answer to [this question](https://stackoverflow.com/questions/29135698/how-does-stdforward-receive-the-correct-argument) an interesting read. – WhozCraig Jul 13 '16 at 06:55
  • @songyuanyao Thats why I used the `enable_if_t` – Curious Jul 13 '16 at 06:55
  • @ChrisBeck because the function reference argument itself is `const`, so just to remove that constness – Curious Jul 13 '16 at 06:56
  • @ChrisBeck or maybe I misunderstood the meaning of `const T&` to mean that the reference itself is const rather than the pointed to object being const. – Curious Jul 13 '16 at 06:57
  • I see so basically what you are saying is: "If this is a non const thing, that can bind to `const T &`, then `std::forward` of it should return an r-value-reference." (Since that's basically what it had to be) – Chris Beck Jul 13 '16 at 06:57
  • Don't you also need a definition for the case when it is `const` ? Maybe I misunderstood – Chris Beck Jul 13 '16 at 06:58
  • @ChrisBeck You mean a definition of std::forward for the case when the passed in object is const? I have never seen it used like that so I disabled it... – Curious Jul 13 '16 at 06:59
  • So what does yours do in a case like this: `void f(const T & t) { g(std::forward(t)); `? I guess it doesn't compile? If I understood right then the original one will still work, but maybe I'm wrong about that. Maybe a more realistic case is, I start with a `const T &`, and pass it via a forwarding reference into some function, where `std::forward` is applied. – Chris Beck Jul 13 '16 at 07:01
  • @ChrisBeck that will compile because the reference itself is not const... let me think about this for one more minute though... – Curious Jul 13 '16 at 07:03
  • @ChrisBeck I guess that is why the 2 overloads are needed? to prevent a user from using the function like that? Since that version would work with `const` values as well, but I do not see why that would be an issue. Maybe because `const Type&&` doesn't really make sense? Although `std::forward` allows that type. – Curious Jul 13 '16 at 07:08
  • I guess I don't know, I never thought about implementing `std::forward`, I just kind of got used to what it does – Chris Beck Jul 13 '16 at 07:10
  • @ChrisBeck same here! – Curious Jul 13 '16 at 07:10
  • Idk, I have to go, I will check back later – Chris Beck Jul 13 '16 at 07:13
  • I don't even know what your version is trying to do; that `static_cast` casts away constness and will never compile. – T.C. Jul 13 '16 at 08:22
  • 2
    The answer is in the link from @WhozCraig, and also in the cppreference.com docs. It's basically not about what happens when you use `forward` as recommended. It's about unusual cases that could be encountered accidentally. One of them would be `forward(function_returning_t())`. In this case, your version (minus the `const` issues) would effectively cast the return from the function to an lvalue; the standard one is specified to issue a compile time error. – bogdan Jul 13 '16 at 08:25
  • See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html. Use Case #D – Arunmu Jul 13 '16 at 08:29
  • Got the above link from this question : http://stackoverflow.com/questions/8283616/why-doesnt-const-int-ci-2-stdforwardintci-work-and-how-to-fix-wor – Arunmu Jul 13 '16 at 08:32
  • Also, it's called "perfect forwarding" because it's supposed to be... well, as close to perfect as possible. It has to forward `const` lvalues and rvalues just as well. What the destination does with them is not `forward`'s business. – bogdan Jul 13 '16 at 08:35

1 Answers1

10

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.

Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977