Flattening a pack of types, where non-type values

2019-08-24 05:16发布

问题:

If you look at the member type of

template <typename Pack> struct flatten;

template <typename T, typename U, std::size_t A, std::size_t B, std::size_t C, typename V, std::size_t D, std::size_t E, typename W>
struct flatten<std::tuple<T,U, std::index_sequence<A,B,C>, V, std::index_sequence<D,E>, W>> {
    template <typename, typename, std::size_t, std::size_t, std::size_t, typename, std::size_t, std::size_t, typename> struct S;
    using type = S<T,U,A,B,C,V,D,E,W>;
};

is it possible to do this generically, thus flattening all types and non-types into a single flattened pack, as long as the non-types are wrapped within a type?

Here is my simple solution to the normal type of flattening, and I'm just wondering how to use this method to work the above generically.

template <typename T> struct identity { using type = T; };

template <typename...> struct merge;

template <typename Pack>
struct merge<Pack> : identity<Pack> {};

template <template <typename...> class P, typename... Ts, typename... Us>
struct merge<P<Ts...>, P<Us...>> : identity<P<Ts..., Us...>> {};

template <typename First, typename... Rest>
struct merge<First, Rest...> : merge<First, typename merge<Rest...>::type> {};

template <typename T, template <typename...> class P> struct flatten_h : identity<P<T>> {};

template <template <typename...> class P, typename... Ts>
struct flatten_h<P<Ts...>, P> : merge<typename flatten_h<Ts, P>::type...> {};   

template <typename Pack> struct flatten;

template <template <typename...> class P, typename... Ts>
struct flatten<P<Ts...>> : flatten_h<P<Ts...>, P> {};

// Testing
#include <type_traits>

template <typename...> struct P;

int main() {
    static_assert(std::is_same<
        flatten<P<int, char, long, P<double, bool>, int>>::type,
        P<int, char, long, double, bool, int>
    >::value);

    static_assert(std::is_same<
        flatten<P<int, P<bool, bool>, long, P<double, P<char, P<long>>>, int>>::type,
        P<int, bool, bool, long, double, char, long, int>
    >::value);
}

I think C++20 should allow auto... (or some other keyword) to indicate both types and non-type values for template arguments.

回答1:

The task becomes feasible if each non-type parameter (unpacked from, say, an std::index_sequence) can be wrapped in its own std::integral_constant. Then there are only type template parameters at the level where the flattening occurs and one may use a simple type container like template<class...> struct Types {};.

// upper version for shorter type names; lower version for showing types
template<auto v> struct Val : std::integral_constant<decltype(v), v> {};
//template<auto v> using Val = std::integral_constant<decltype(v), v>;
// NOTE: bug(?) in GCC 7.2 which maps size_t(0) to false and size_t(1) to true

template<class I, I... is>
auto flatten(Type< std::integer_sequence<I, is...> >) {
  return Flattened<Val<is>...>{};
}

Below and in a live demo you find a full working example with that restriction (and the bug(?) mentioned in the comment).

I chose to specify all types which should be flattened. Alternatively, one may also blindly unpack "everything reasonable" using various template template arguments of the form template<template<auto, class...> class ToFlatten> etc.

#include <iostream>
#include <tuple>
#include <utility>

// upper version for shorter type names; lower version for showing types
template<auto v> struct Val : std::integral_constant<decltype(v), v> {};
//template<auto v> using Val = std::integral_constant<decltype(v), v>;
// NOTE: bug(?) in GCC 7.2 which maps size_t(0) to false and size_t(1) to true

template<size_t... is, class F>
constexpr decltype(auto) indexer_impl(std::index_sequence<is...>, F f) {
  return f(Val<is>{}...);
}

template<size_t size, class F>
constexpr decltype(auto) indexer(F f) {
  return indexer_impl(std::make_index_sequence<size>{}, f);
}

////////////////////////////////////////////////////////////////////////////////

template<class T_>
struct Type {};

template<class... Ts>
struct Types {
  static constexpr auto size = Val<sizeof...(Ts)>{};
  using Tuple = std::tuple<Ts...>;
};

template<size_t i, class T>
using at_t = std::tuple_element_t<i, typename T::Tuple>;

////////////////////////////////////////////////////////////////////////////////

template<class...> struct Flattened;

template<class I, I... is> using int_seq = std::integer_sequence<I, is...>;

// specify which types are allowed in a flat type container
template<class T> struct is_flat : Val<true> {};
template<class I, I... is> struct is_flat< int_seq<I, is...> > : Val<false> {};
template<class... Ts> struct is_flat< Types<Ts...> > : Val<false> {};
template<class... Ts> struct is_flat< Flattened<Ts...> > : Val<false> {};

// check if a type is an instantiation of `template<class...> struct Flattened`
template<class T> struct is_flattened : Val<false> {};
template<class... Ts> struct is_flattened<Flattened<Ts...>> : Val<true> {};

// specific type container which guarantees to contain `is_flat` types only
template<class... Ts> struct Flattened : Types<Ts...> {
  static_assert((... && is_flat<Ts>{}));
};

////////////////////////////////////////////////////////////////////////////////

namespace internal {

auto merge() {
  return Flattened<>{};
}

template<class... Ts>
auto merge(Flattened<Ts...> done) {
  return done;
}

template<class... Ts, class... Us>
auto merge(Flattened<Ts...>, Flattened<Us...>) {
  return Flattened<Ts..., Us...>{};
}

// merge more than two args: attempt to avoid linear recursion: is it better?
template<class... Ts, class... Fs>
auto merge(Flattened<Ts...>, Fs...) {
  static_assert((... && is_flattened<Fs>{}));

  using T = Types<Flattened<Ts...>, Fs...>;

// group the Flattened args into two halves
  constexpr size_t N = T::size;
  constexpr size_t N0 = N/2u;
  constexpr size_t N1 = N-N0;

  auto h0 = indexer<N0>([] (auto... is) { return merge(at_t<is, T>{}...); });
  auto h1 = indexer<N1>([] (auto... is) { return merge(at_t<N0+is, T>{}...); });

  return merge(h0, h1);
}

template<class T>
auto flatten(Type<T>) {
  static_assert(is_flat<T>{});
  return Flattened<T>{};
}

template<class I, I... is>
auto flatten(Type< std::integer_sequence<I, is...> >) {
  return Flattened<Val<is>...>{};
}

template<class... Ts>
auto flatten(Type< Types<Ts...> >) {
  return merge(internal::flatten(Type<Ts>{})...);
}

}// internal

template<class... Ts>
auto flatten(Types<Ts...>) {
  return internal::merge(internal::flatten(Type<Ts>{})...);
}

////////////////////////////////////////////////////////////////////////////////

template<class T>
void inspect() {
  std::cout << __PRETTY_FUNCTION__ << std::endl;
}

struct Custom {};

int main() {
  auto foo = Types<
    Types<int, char, long>,
    Val<7>,
    Types<double, Val<5>, float, Types<unsigned, Types<Custom, Types<char>, int>>, std::make_index_sequence<4u>>,
    std::index_sequence<5u,19u,4u>,
    Types<>,
    Val<8>
  >{};

  auto bar = flatten(foo);
  inspect<decltype(bar)>();

  return 0;
}

Output:

void inspect() [with T = Flattened<int, char, long int, Val<7>, double, Val<5>, float, unsigned int, Custom, char, int, Val<false>, Val<true>, Val<2>, Val<3>, Val<5>, Val<19>, Val<4>, Val<8> >]

Output with longer type names:

void inspect() [with T = Flattened<int, char, long int, std::integral_constant<int, 7>, double, std::integral_constant<int, 5>, float, unsigned int, Custom, char, int, std::integral_constant<bool, false>, std::integral_constant<bool, true>, std::integral_constant<long unsigned int, 2>, std::integral_constant<long unsigned int, 3>, std::integral_constant<int, 5>, std::integral_constant<long unsigned int, 19>, std::integral_constant<long unsigned int, 4>, std::integral_constant<int, 8> >]