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.
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));
}
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.
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));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With