Search code examples
c++c++14perfect-forwardingtype-deduction

How to construct an object either from a const reference or temporary via forwarding template


Consider this minimal example

template <class T>
class Foo
{
public:
    Foo(const T& t_)
        : t(t_)
    {
    }

    Foo(T&& t_)
        : t(std::move(t_))
    {
    }

    T t;
};

template <typename F>
Foo<F> makeFoo(F&& f)
{
    return Foo<F>(std::forward<F>(f));
}

int main()
{
    class C
    {

    };

    C c;

    makeFoo(c);
}

MSVC 2017 fails with a redefinition error of Foo's ctor. Apparently T gets deduced to C& instead of the intended C. How exactly does that happen and how to modify the code so that it does what is inteded: either copy construct Foo::t from a const reference or move construct it from an r-value.


Solution

  • template <class F, class R = std::decay_t<F>>
    Foo<R> makeFoo(F&& f)
    {
      return Foo<R>(std::forward<F>(f));
    }
    

    that is a clean and simple way to solve your problem.

    Decay is an appropriate way to convert a type into a type suitable for storing somewhere. It does bad things with array types but otherwise does pretty much the right thing; your code doesn't work with array types anyhow.


    The compiler error is due to reference collapsing rules.

     X          X&          X const&       X&&
     int        int&        int const&     int&&
     int&       int&        int&           int&
     int const  int const&  int const&     int const&&
     int&&      int&        int&           int&&
     int const& int const&  int const&     int const&
    

    these may seem strange.

    The first rule is that a const reference is a reference, but a reference to const is different. You cannot qualify the "reference" part; you can only const-qualify the referred part.

    When you have T=int&, when you calculate T const or const T, you just get int&.

    The second part has to do with how using r and l value references together work. When you do int& && or int&& & (which you cannot do directly; instead you do T=int& then T&& or T=int&& and T&), you always get an lvalue reference -- T&. lvalue wins out over rvalue.

    Then we add in the rules for how T&& types are deduced; if you pass a mutable lvalue of type C, you get T=C& in the call to makeFoo.

    So you had:

    template<F = C&>
    Foo<C&> makeFoo( C& && f )
    

    as your signature, aka

    template<F = C&>
    Foo<C&> makeFoo( C& f )
    

    now we examine Foo<C&>. It has two ctors:

    Foo( C& const& )
    Foo( C& && )
    

    for the first one, const on a reference is discarded:

    Foo( C& & )
    Foo( C& && )
    

    next, a reference to a reference is a reference, and lvalue references win out over rvalue references:

    Foo( C& )
    Foo( C& )
    

    and there we go, two identical signature constructors.

    TL;DR -- do the thing at the start of this answer.