c++tuplesc++17flattenperfect-forwarding

How to flatten heterogeneous lists (aka tuples of tuples of ...)


I am attempting to employ C++17 fold expressions and the C++14 indices trick to flatten an arbitrary input consisting of tuples and non-tuples.

The expected result should at least conform to these requirements:

constexpr auto bare = 42;

constexpr auto single = std::tuple{bare};
constexpr auto nested_simple = std::tuple{single};

constexpr auto multiple = std::tuple{bare, bare};
constexpr auto nested_multiple = std::tuple{multiple};

constexpr auto multiply_nested = std::tuple{multiple, multiple};

static_assert(flatten(bare) == bare);
static_assert(flatten(single) == bare);
static_assert(flatten(nested_simple) == bare);

static_assert(flatten(multiple) == multiple);
static_assert(flatten(nested_multiple) == multiple);

static_assert(flatten(multiply_nested) == std::tuple{bare, bare, bare, bare});

I have relatively simple code to handle all but the last case:

template<typename T>
constexpr decltype(auto) flatten(T&& t)
{
    return std::forward<T>(t);
}

template<typename T>
constexpr decltype(auto) flatten(std::tuple<T> t)
{
    return std::get<0>(t);
}

template<typename... Ts>
constexpr decltype(auto) flatten_multi(Ts&&... ts)
{
    return std::make_tuple(flatten(ts)...);
}

template<typename... Ts, std::size_t... Indices>
constexpr decltype(auto) flatten_impl(std::tuple<Ts...> ts, const std::index_sequence<Indices...>&)
{
    return flatten_multi(std::get<Indices>(ts)...);
}

template<typename... Ts>
constexpr decltype(auto) flatten(std::tuple<Ts...> ts)
{
    return flatten_impl(ts, std::make_index_sequence<sizeof...(Ts)>());
}

Live demo here. Obviously, it doesn't handle multiply nested items well.

The more advanced form to handle the multiply_nested case I haven't found. I tried applying operator>> to be able to use fold expressions, but haven't been able to get anything that compiles. My last attempt can be found here. The core idea is to use operator>> in a fold expression to combine elements 2 by 2, each time unwrapping the previous result.

It seems to me I should be able to use something like std::tuple_cat, but it shouted at me quite loudly for reasons I couldn't decipher completely.

So my question is this: what am I missing? How can I unwrap an arbitrarily deeply arbitrarily nested tuple-like input?


Solution

  • I propose to SFINAE on presence of tuple

    // Simple traits
    template <typename T> struct is_tuple : std::false_type{};
    template <typename... Ts> struct is_tuple<std::tuple<Ts...>> : std::true_type{};
    
    // utility to ensure return type is a tuple
    template<typename T>
    constexpr decltype(auto) as_tuple(T t) { return std::make_tuple(t); }
    
    template<typename ...Ts>
    constexpr decltype(auto) as_tuple(std::tuple<Ts...> t) { return t; }
    
    // Simple case
    template<typename T>
    constexpr decltype(auto) flatten(T t)
    {
        return t;
    }
    
    // Possibly recursive tuple
    template<typename T>
    constexpr decltype(auto) flatten(std::tuple<T> t)
    {
        return flatten(std::get<0>(t));
    }
    
    // No more recursion, (sizeof...Ts != 1) with above overload
    template<typename ...Ts, std::enable_if_t<!(is_tuple<Ts>::value || ...), bool> = false>
    constexpr decltype(auto) flatten(std::tuple<Ts...> t)
    {
        return t;
    }
    
    // Handle recursion
    template<typename ...Ts, std::enable_if_t<(is_tuple<Ts>::value || ...), bool> = false>
    constexpr decltype(auto) flatten(std::tuple<Ts...> t)
    {
        return std::apply([](auto...ts)
                          {
                              return flatten(std::tuple_cat(as_tuple(flatten(ts))...));
                          }, t);
    }
    

    Demo