Mohammad Alavi
Mohammad Alavi

Reputation: 1330

Why doesn't std::forward preserve the lvalue-ness of this variable?

In the code below (which is run in C++20), when I call the UseForward function, I expect the first overload to be called (which is template <typename T> void UseForward(T& value)) and it does get called. Then in the body of the function I use std::forward which I expect to preserve the lvalue-ness of variable value and call the copy constructor, BUT it calls the move constructor. What am I missing here?

class SomeClass
{
public:
    SomeClass()
    {
        std::cout << "default constructor" << std::endl;
    }

    SomeClass(const SomeClass& other)
    {
        std::cout << "const copy constructor" << std::endl;
    }

    SomeClass(SomeClass&& other) noexcept
    {
        std::cout << "move constructor" << std::endl;
    }
};

template <typename T>
void UseForward(T& value)
{
    std::cout << "UseForward pass by reference!" << std::endl;
    auto sc = SomeClass(std::forward<T>(value));
}

template <typename T>
void UseForward(T&& value)
{
    std::cout << "UseForward pass by rvalue reference!" << std::endl;
    auto sc2 = SomeClass(std::forward<T>(value)); 
}

int main()
{
    auto sc = SomeClass();
    UseForward(sc);
}

Upvotes: 2

Views: 162

Answers (3)

Swift - Friday Pie
Swift - Friday Pie

Reputation: 14688

Comment out first definition of UseForward and behold, both

UseForward(sc);
UseForward(SomeClass());

would say that you had passed value by rvalue reference. That's because && with cv-unqualified type T (T&&) in substitution context is NOT an rvalue reference but so-called forwarding reference, it preserves a value cathegory.

For desired effect you have to use type traits, by SFINAE or by compile-time alternatives:

template <typename T>
void UseForward(T&& value)
{
    if constexpr (std::is_rvalue_reference_v<decltype(value)>)
        std::cout << "UseForward pass by rvalue reference!" << std::endl;
    else
        std::cout << "UseForward pass by reference!" << std::endl;
    auto sc2 = SomeClass(std::forward<T>(value)); 
}

Upvotes: 4

Jan Schultke
Jan Schultke

Reputation: 39869

As the other answers have already explained, you're misusing std::forward inside UseForward(T&), and it effectively behaves like std::move. Removing that function ends up calling UseForward(T&&), which accepts a forwarding reference (not an rvalue reference), and std::forward works correctly.

How std::forward is expected to be used

It's also worth noting why that actually happens, and why std::forward is designed like that. std::forward expects either:

  • std::forward<T>(t) where t is a forwarding reference, or
  • std::forward<decltype(t)>(t) where t is any expression

How std::forward interacts with forwarding references

Inside UseForward(T&&), when called with an lvalue of type SomeClass, T will deduce to SomeClass&. Due to reference collapsing, a T&& where T = SomeClass& (i.e. rvalue ref. + lvalue ref.), collapses to SomeClass&. This means you are calling std::forward<SomeClass&>(SomeClass&), which results in an lvalue.

However, in UseForward(T&), T will simply deduce to SomeClass, and you are calling std::forward<SomeClass>(SomeClass&). In such a case, std::forward will result in an xvalue, and you end up calling the move constructor.

const-correctness can save you trouble

On a side note, you wouldn't have accidentally called the move constructor if your overloads consisted of:

  • UseForward(const T&) and
  • UseForward(T&&)

Even if you had misused std::forward inside the former function, the const would have prevented you from calling the move constructor. In general, you almost never want to overload between mutable lvalue/rvalue references, but rather between const lvalue references, and mutable rvalue references.

Conclusion

Never use std::forward unless working with forwarding references. Instead:

  • if something is known to be an lvalue reference, don't forward at all
  • if something is known to be an rvalue reference, "forward" by std::move
  • otherwise, use std::forward for forwarding references as intended
template <typename T>
void UseForward(T&& value)
{
    std::cout << "UseForward pass by forwarding reference!" << std::endl;
    auto sc2 = SomeClass(std::forward<T>(value)); 
}

Upvotes: 1

viraltaco_
viraltaco_

Reputation: 1200

The answer is: you're asking it to.
std::forward<T>(v) where decltype (v) is T&.
See: std::forward

template <typename T>
void UseForward(T& value) {
    std::cout << "UseForward pass by reference!" << std::endl;
    auto sc = SomeClass(std::forward<decltype (value)>(value));
}

Live on compiler explorer

Upvotes: 3

Related Questions