Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 701
user1709708 Avatar asked Aug 01 '19 15:08

user1709708


3 Answers

In C++17 you can simply write:

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

because of class template argument deduction.


In C++14 you can write:

template <typename F>
auto makeFoo(F&& f)
{
    return Foo<std::decay_t<F>>(std::forward<F>(f));
}
like image 70
BiagioF Avatar answered Oct 05 '22 06:10

BiagioF


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.

like image 28
Yakk - Adam Nevraumont Avatar answered Oct 05 '22 06:10

Yakk - Adam Nevraumont


Issue is that typename provided to class is reference in one case:

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

becomes

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

You probably want some decay:

template <typename F>
Foo<std::decay_t<F>> makeFoo(F&& f)
{
    return Foo<std::decay_t<F>>(std::forward<F>(f));
}
like image 41
Jarod42 Avatar answered Oct 05 '22 06:10

Jarod42