In a code like this:
template<class...> struct pack{};
template<class, class = int>
struct call_t
{
template<class... args_t>
static int apply(args_t&&...)
{ return 0; }
};
template<class... args_t>
struct call_t<pack<args_t...>, // (1)
std::decay_t<decltype(convert(declval<args_t>()...))> >
{
template<class... params_t> // (2)
static int apply(params_t&&... args)
{ return convert(std::forward<params_t>(args)...); }
};
template<class... args_t>
auto test(args_t&&... args) // (3)
{
return call_t<pack<args_t...> >::
apply(std::forward<args_t>(args)...);
}
This function sends a pack of params to a function or another according to if a function convert
exists and can be called with the passed params, keeping intact (I guess) their exact passed types, and when its return type is int
, irrespective of reference or const qualifiers.
I have three doubts with that piece of code.
(1) Is declval
return type still a universal reference? For instance, declval<T>()
, with T = int&
, will its return type be int&&
(a true r-value reference), or int & &&
and be deduced again as int&
following the usual rules of universal references when passing to another call? I supposed that it doesn't (as pointed out by @101010), but I don't know in that case how to make a perfect overloading test.
(2) Do I need to re-specify the variadic template to use the universal reference deduction rules, or since the correct types have been already deduced in (3), they keep their deduced types intact?
Or can I just write
template<class... args_t>
struct call_t<pack<args_t...>, // (1)
std::decay_t<decltype(convert(declval<args_t>()...))> >
{
// (2)
static int apply(args_t... args)
{ return convert(args...); }
};
?
The call_t
template class is an implementation detail, so, it will be instantiate only inside test
.
Those two cases are not equivalent. This example:
template<class... args_t>
struct call_t<pack<args_t...>, // (1)
std::decay_t<decltype(convert(declval<args_t>()...))> >
{
// (2)
static int apply(args_t... args)
{ return convert(args...); }
};
does not forward anything. The parameter pack args...
are lvalues, because they have names. That is very different than this code:
template<class... params_t> // (2)
static int apply(params_t&&... args)
{ return convert(std::forward<params_t>(args)...); }
in which args...
is forwarded. The resulting behavior is that the new example could be slower than the old example (doing copies instead of moves) or could simply surprisingly fail to compile. Consider convert(std::unique_ptr<int> )
. That is invocable with args_t = {std::unique_ptr<int>}
, but the internal apply()
would fail because you'd be trying to copy the unique_ptr
.
You'd need to do:
static int apply(args_t... args)
{ return convert(std::forward<args_t>(args)...); }