4
\$\begingroup\$

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.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ This is a bit above my C++ skills, but I had a thought about: // workaround for C++17 not having lambdas with template args // pass a nullptr of type Ts to allow for type deduction and wondered if you could use a functor is some way (overload [] or ()) but this post Looks very cool BTW. \$\endgroup\$ Commented May 13, 2023 at 18:11
  • \$\begingroup\$ Good suggestion. That does work, and allows us to implement DoReduce as return (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\$ Commented May 16, 2023 at 0:43

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.