Since c++11 parameter pack was introduced. It can be used to store non-type template values, type templates, template templates or function arguments. In this post I will describe how to access parameter pack elements by index in function call.
First element
Accessing front is a simple query. In fact primitive implementation would not even require variadic template.
namespace fused {
template <typename Tp>
constexpr decltype(auto) front(Tp&& t, ...) noexcept;
}
First argument Tp&& t
is of deduced type Tp&&
.
When Tp
is a template parameter Tp&&
it is not just
rvalue reference, but forwarding reference
that can bind to both prvalue/xvalue and lvalue references.
Body of function fused::front
is a one-liner just
returning t
by std::forward cast.
The key thing here is how to pass anything else beside t
in a generic way. Passing via c-style ellipsis
will swallow trailing arguments, but it does not work for
uncopyable types like std::unique_ptr:
error: cannot pass objects of non-trivially-copyable type %T through ‘...’
Following implementation overcomes this problem
with unnamed parameter pack Us&&...
:
namespace fused {
template <typename Tp, typename... Us>
constexpr decltype(auto) front(Tp&& t, Us&&...) noexcept {
return std::forward<Tp>(t);
}
}
Us&&...
expands to any number of forwarding references.
// test
// bind rvalue
auto x = fused::front(unsigned{1u}, 2.0, std::make_unique<char>('3'));
assert(x == 1u);
// bind lvalue
unsigned a = 4u;
auto const& y = fused::front(a, 2.0, std::make_unique<char>('3'));
assert(y == 4u);
assert(&a == &y);
second
Accessing second argument is not so trivial as fused::front
.
Ellipsis or parameter pack must appear last,
so it cannot be used to swallow non-trailing arguments.
To swallow single argument in front meta::ignore
can be used:
namespace meta {
struct ignore final {
template <typename... Ts>
constexpr ignore(Ts&&...) noexcept {}
};
}
Job of meta::ignore
is to literally do nothing. It has only
a constructor, that allows to create meta::ignore
instance
of any arguments without modifying them. It is possible to
implicitly convert any type to meta::ignore
.
namespace fused {
template <typename Tp>
decltype(auto) second(meta::ignore, Tp&& t, ...) {
return std::forward<Tp>(t);
}
}
// test
// implicitly convert first argument to ignore
auto sec = fused::second(std::make_unique<unsigned>(1u),
2.0, '3');
assert(sec == 2.0);
back
Accessing last argument of function call requires swallowing
all preceding ones. N-argument
function signature should consist of N-1 meta::ignore
s
and single trailing argument that will be returned.
Variadic template cannot be followed by a single parameter,
fused::back
uses a little trick to do so.
namespace fused {
template <typename Tp, typename... Ts>
constexpr decltype(auto) back(Tp&& t, Ts&&... ts) noexcept;
}
Function fused::back
expects at least one argument
and potentially zero-length tail. Additional consequence of above
N-argument function signature split is that length of tail, sizeof...(Ts)
,
is exactly equal to required N-1.
Wrapping meta::ignore
in simple template alias allows to generate numerous
implicit conversions regardless of used type.
namespace meta {
template <typename>
using eat = ignore;
template <std::size_t>
using eat_n = ignore;
}
Function fused::back
internally contains a lambda that
takes N-1 meta::eat
s and a trailing argument of auto&&
type.
Only last argument is returned.
namespace fused {
template <typename Tp, typename... Us>
constexpr decltype(auto) back(Tp&& t, Us&&... us) noexcept {
return [](meta::eat<Us>..., auto&& x) -> decltype(x) {
return std::forward<decltype(x)>(x);
}(std::forward<Tp>(t), std::forward<Us>(us)...);
}
}
Internal lambda is called in place
by forwarding Tp&& t
and all Us&&... us
directly from fused::back
.
The trick here is to use fused::back
arguments split to generate required number of meta::eat
s.
// test
auto a = 1u;
auto b = 2.0;
auto c = fused::back(1u, 2.0, '3');
assert(c == '3');
auto const& x = fused::back(a, b, c);
assert(x == '3');
assert(&c == &x);
auto y = fused::back(a, b, std::make_unique<char>('4'));
assert(y != nullptr);
assert(*y == '4');
nth
To access argument at any given index N of function call both techniques (skipping trailing and preceding) must be combined.
namespace detail {
template<std::size_t... skip>
struct at {
template<typename Tp, typename... Us>
constexpr decltype(auto) operator()(meta::eat_n<skip>...,
Tp&& x, Us&&...) const noexcept {
return std::forward<Tp>(x);
}
};
}
The problem is how to supply required number of values
when instantiating detail::at
template?
It can be done with use of
std::make_index_sequence,
default template parameter and partial specialization.
namespace detail {
template <std::size_t N, typename = std::make_index_sequence<N>>
struct at;
template <std::size_t N, std::size_t... skip>
struct at<N, std::index_sequence<skip...>> {
template <typename Tp, typename... Us>
constexpr decltype(auto) operator()(meta::eat_n<skip>..., Tp&& x,
Us&&...) const noexcept {
return std::forward<Tp>(x);
}
};
}
Instantiating detail::at
with single template parameter N
will
use default value for the second one, producing a list of integers
in range [0, N)
. This list allows to expand meta::eat_n
in function call signature for each index up to N-1.
Next argument Tp&& x
, at index N, will be returned.
Following trailing arguments will be swallowed.
Generalized access of functiona argument parameter pack
can be done with fused::nth
.
namespace fused {
template <std::size_t N, typename... Ts>
decltype(auto) nth(Ts&&... args) {
return detail::at<N>{}(std::forward<Ts>(args)...);
}
}
Template parameters of fused::nth
are partially deduced.
Trailing variadic template typename... Ts
can be deduced from
given arguments Ts&&... args
when fused::nth
is called.
Compiler cannot guess non-type template parameter std::size_t N
,
user has to pass it explicitly.
std::make_unique or
std::get work in similar way.
// test
auto a = 1u;
auto b = 2.0;
auto c = '3';
auto const& x = fused::nth<0>(a, b, c);
auto const& y = fused::nth<1>(a, b, c);
auto const& z = fused::nth<2>(a, b, c);
assert(&a == &x);
assert(&b == &y);
assert(&c == &z);
fused::nth<0>(a, b, c) = 7u;
assert(a == 7u);
Try this code!