Sometimes I find I want to call a function passing each of a set of types as a template parameter, but without needing to construct an object of those types. I also may want to do this in multiple places, or have a class that holds a tuple of these types, but I want to have this type list defined in a single place, not multiple places, needing to keep the duplicate lists synchronised.
This could be done with a #define, but I'd rather avoid macros if there's a proper code solution. As noted in some of my code comments, a C++20 solution does provide a better/cleaner interface, but I think this C++17 solution still has value.
#include <tuple>
#include <type_traits>
template <typename... Ts> struct pack {};
namespace detail {
template <typename>
struct TupleFromPackImpl;
template <typename... Ts>
struct TupleFromPackImpl<pack<Ts...>>
{
using Type = std::tuple<Ts...>;
};
template <typename>
struct ReduceAcrossPackTypesImpl;
template <typename... Ts>
struct ReduceAcrossPackTypesImpl<pack<Ts...>>
{
template <typename F>
constexpr static decltype(auto) DoReduce(F&& f)
{
return (f(static_cast<Ts*>(nullptr)...));
}
};
}
template <typename PackT>
using TupleFromPack = typename detail::TupleFromPackImpl<PackT>::Type;
// workaround for C++17 not having lambdas with template args
// pass a nullptr of type Ts* to allow for type deduction
//
// to be called with a lambda like
// [](auto*... t){ (f<std::remove_pointer_t<decltype(t)>>() op ...); }
//
// with C++20 we can change this so we can instead call with code like
// []<typename... Ts>(){ (f<Ts>() op ...); }
template <typename PackT, typename F>
constexpr decltype(auto) ReduceAcrossPackTypes(F&& f)
{
return detail::ReduceAcrossPackTypesImpl<PackT>::DoReduce(std::forward<F>(f));
}
// to be called with a lambda like
// [](auto* t){ f<std::remove_pointer_t<decltype(t)>>(); }
//
// with C++20 we can change this so we can instead call with code like
// []<typename T>(){ f<T>(); }
template <typename PackT, typename F>
constexpr void ForEachTypeInPack(F&& f)
{
ReduceAcrossPackTypes<PackT>([&f](auto*... ts){ (f(ts), ...); });
}
static_assert(std::is_same_v<TupleFromPack<pack<int, double>>, std::tuple<int, double>>);
static_assert(ReduceAcrossPackTypes<pack<int, long, unsigned>>([](auto*... ts){ return (std::is_integral_v<std::remove_pointer_t<decltype(ts)>> && ...); }));
static_assert(!ReduceAcrossPackTypes<pack<int, long, double>>([](auto*... ts){ return (std::is_integral_v<std::remove_pointer_t<decltype(ts)>> && ...); }));
As an addendum: my ideal syntax for something like this would be the ability to typedef a parameter pack - something like
using... MyTypes = (int, long, unsigned);
using MyTuple = std::tuple<MyTypes...>;
static_assert((std::is_integral_v<MyTypes> && ...));
completely eliminating the need for any trickery to be able to do this.
DoReduceasreturn (f.template operator()<Ts...>());, but then to be strictly conforming to C++17 there's no way to call it with a lambda - you have to create a functor struct. Luckily, I've also found that gcc allows the C++20 syntax with no warning, and clang allows it with an easily disable-able -Wc++20-extensions warning, so for my purposes I can just use the C++20 syntax and disable that clang warning. \$\endgroup\$