Search code examples
c++templatesc++17sfinaefold-expression

Conditionally remove function calls in fold expression


I know that I can use SFINAE to disable generation of templated functions based on a condition, but that doesn't really work in this case. I want to initialize an array at compile-time that should contain values that matches a condition. Something like this:

template <std::size_t i, class ... Types, class ... Group>
constexpr auto fetch_match(const std::tuple<Group...>& candidates)
{
    if constexpr (is_match<std::tuple<Group...>, i, Types...>())
    {
        auto& group = std::get<i>(candidates);
        return group.template get<Types...>();
    }
}

template <class ... Types, class ... Group, std::size_t ... indices>
constexpr auto get_matches(const std::tuple<Group...>& candidates, std::index_sequence<indices...>)
{
    constexpr std::array views {
        (fetch_match<indices, Types...>(candidates), ...),
    };
    return views;
}

I know the code above is wrong and doesn't compile. If the condition isn't filled, then I want the fold expression to not generate that function call. How would I do that?


This question might be an XY-problem, so here's a the problem in more detail.

I have a Registry that contains Groups of heterogeneous data. I want to be able to query all groups that contains the specified sub list of types. For example, for (const auto& view : registry.get<char, short, int>()) should yield an array with views of the groups that contain char, short and int. I've created a mcve below. The problem with the current code is that I have to first create the array and then copy the views, which I'd like to avoid.

#include <tuple>
#include <array>
#include <utility>
#include <type_traits>
#include <iostream>


template <typename T, typename... Ts>
constexpr bool contains = (std::is_same<T, Ts>{} || ...);

template <typename Subset, typename Set>
constexpr bool is_subset_of = false;

template <typename... Ts, typename... Us>
constexpr bool is_subset_of<std::tuple<Ts...>, std::tuple<Us...>> = (contains<Ts, Us...> && ...);

template <typename ... T>
struct View
{
    const char* name_of_group;  // For debugging.
    std::tuple<T...> data;
};

template <typename ... Ts>
struct Group
{
    using type_set = std::tuple<Ts...>;
    static const char* name;   // For debugging.

    std::tuple<Ts...> data;

    explicit Group(Ts... values) : data(values...) {}

    template <typename ... Us>
    [[nodiscard]] View<Us...> get() const noexcept
    {
        return { this->name, std::make_tuple(std::get<Us>(this->data)...) };
    }
};

template <class Groups, std::size_t i, class ... Types>
constexpr bool is_match()
{
    using group_type = std::tuple_element_t<i, Groups>;
    bool match = is_subset_of<std::tuple<Types...>, typename group_type::type_set>;
    return match;
}


template <std::size_t i, class ... Types, class ... Group, class Array>
constexpr void add_matches(const std::tuple<Group...>& candidates, Array& matches, std::size_t& index)
{
    if constexpr (is_match<std::tuple<Group...>, i, Types...>())
    {
        auto& group = std::get<i>(candidates);
        matches[index++] = group.template get<Types...>();
    }
}

template <class ... Types, class ... Group, std::size_t ... indices>
constexpr auto get_matches(const std::tuple<Group...>& candidates, std::index_sequence<indices...>)
{
    constexpr std::size_t size = (is_match<std::tuple<Group...>, indices, Types...>() + ... + 0);
    std::array<View<Types...>, size> views {};
    std::size_t index = 0;
    (add_matches<indices, Types...>(candidates, views, index), ...);
    return views;
}


template <typename ... Group>
class Registry
{
public:
    explicit Registry(Group... groups) : groups(groups...) {}

    template <typename ... T>
    auto get()
    {
        constexpr auto indices = std::index_sequence_for<Group...>{};
        return get_matches<T...>(this->groups, indices);
    }

private:
    std::tuple<Group...> groups;
};


using A = Group<char>;
using B = Group<char, short>;
using C = Group<char, short, int>;
using D = Group<char, short, int, long long>;

// Giving the classes names for debugging purposes.
template<> const char* A::name = "A";
template<> const char* B::name = "B";
template<> const char* C::name = "C";
template<> const char* D::name = "D";


int main()
{
    auto registry = Registry(A{0}, B{1,1}, C{2,2,2}, D{3,3,3,3});

    // Should yield an array of size 2 with View<char, short, int>,
    // one from group C and one from Group D.
    for (const auto& view : registry.get<char, short, int>())
    {
        std::cout << "View of group: " << view.name_of_group     << std::endl;
        std::cout << "char: "  << int(std::get<char>(view.data)) << std::endl;
        std::cout << "short: " << std::get<short>(view.data)     << std::endl;
        std::cout << "int: "   << std::get<int>(view.data)       << std::endl;
    }
}

Trying the suggestion in the comments, the following code is as far as I got.

template <class Groups, std::size_t i, class ... Types>
constexpr bool is_match()
{
    using group_type = std::tuple_element_t<i, Groups>;
    bool match = is_subset_of<std::tuple<Types...>, typename group_type::type_set>;
    return match;
}
template <class ... Types, class ... Group, std::size_t ... indices>
constexpr auto build_view_array(const std::tuple<Group...>& candidates, std::index_sequence<indices...>)
{
    std::array views {
            std::get<indices>(candidates).template get<Types...>()...
    };
    return views;
}
template <std::size_t i, class Groups, class TypeSet, std::size_t ... x>
constexpr auto get_matching_indices()
{
    if constexpr (is_match<Groups, i, TypeSet>())
        return std::index_sequence<x..., i>{};
    else
        return std::index_sequence<x...>{};
}
template <std::size_t i, std::size_t j, std::size_t ... rest, class Groups, class TypeSet, std::size_t ... x>
constexpr auto get_matching_indices()
{
    if constexpr (is_match<Groups, i, TypeSet>())
        return get_matching_indices<j, rest..., Groups, TypeSet, i, x...>();
    else
        return get_matching_indices<j, rest..., Groups, TypeSet, x...>();
}

template <class ... Types, class ... Group, std::size_t ... indices>
constexpr auto get_matches(const std::tuple<Group...>& candidates, std::index_sequence<indices...>)
{
    constexpr auto matching_indices = get_matching_indices<indices..., std::tuple<Group...>, std::tuple<Types...>>();
    constexpr auto views = build_view_array<Types...>(candidates, matching_indices);
    return views;
}

It feels like it should work, but it won't compile due to the following error:

/Users/tedkleinbergman/Programming/ECS/temp.cpp:76:39: error: no matching function for call to 'get_matching_indices'
    constexpr auto matching_indices = get_matching_indices<indices..., std::tuple<Group...>, std::tuple<Types...>>();
                                      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/tedkleinbergman/Programming/ECS/temp.cpp:92:16: note: in instantiation of function template specialization 'get_matches<char, short, int, Group<char>, Group<char, short>, Group<char, short, int>, Group<char, short, int, long long> , 0, 1, 2, 3>' requested here
        return get_matches<T...>(this->groups, indices);
               ^
/Users/tedkleinbergman/Programming/ECS/temp.cpp:118:38: note: in instantiation of function template specialization 'Registry<Group<char>, Group<char, short>, Group<char, short, int>, Group<char, short, int, long long> >::get<char, short, int>' requested here
    for (const auto& view : registry.get<char, short, int>())
                                     ^
/Users/tedkleinbergman/Programming/ECS/temp.cpp:57:16: note: candidate template ignored: invalid explicitly-specified argument for template parameter 'Groups'
constexpr auto get_matching_indices()
               ^
/Users/tedkleinbergman/Programming/ECS/temp.cpp:65:16: note: candidate template ignored: invalid explicitly-specified argument for template parameter 'rest'
constexpr auto get_matching_indices()
               ^
1 error generated.

Solution

  • First, start with an index_sequence filter:

    template<std::size_t I>
    using index_t = std::integral_constant<std::size_t, I>;
    template<std::size_t I>
    constexpr index_t<I> index = {};
    
    template<std::size_t...Is, std::size_t...Js>
    constexpr std::index_sequence<Is...,Js...> concatenate( std::index_sequence<Is...>, std::index_sequence<Js...> ) {
      return {};
    }
    template <class Test>
    constexpr auto filter_sequence(std::index_sequence<> sequence, Test test) {
      return sequence;
    }
    
    template<std::size_t I0, std::size_t...Is, class Test>
    constexpr auto filter_sequence( std::index_sequence<I0, Is...>, Test test )
    {
      constexpr auto tail = filter_sequence( std::index_sequence<Is...>{}, test );
      if constexpr ( test(index<I0>) ) {
        return concatenate( std::index_sequence<I0>{}, tail );
      } else {
        return tail;
      }
    }
    

    we then use these primitives.

    template <class Group, class ... Types>
    constexpr auto get_match_indexes()
    {
      constexpr auto test = [](auto I){ return is_match<Group, I, Types...>(); };
      constexpr auto indexes = std::make_index_sequence< std::tuple_size_v<Group> >{};
      constexpr auto retval = filter_sequence( indexes, test );
      return retval;
    }
    template<class ... Types, class Group, std::size_t...Is>
    std::array<sizeof...Is, View<Types...>> get_matches(const Group& candidates, std::index_sequence<Is...> ) {
      return {{
        std::get<Is>(candidates).template get<Types...>(), ...
      }};
    }
    template<class ... Types, class Group>
    std::array<sizeof...Is, View<Types...>> get_matches(const Group& candidates ) {
      return get_matches<Types...>( candidates, get_match_indexes<Group, Types...>() );
    }
    

    or something like that.

    Note that some compilers may need to replace is_match<Group, I, Types...>() with is_match<Group, decltype(I)::value, Types...>().

    There may be typos. This uses at the least.

    filter_sequence uses O(n^2) template symbol length and O(n) recursive template instantiation depth. It can be improved to O(n lg n) length and O(lg n) depth with a tricky code; basically, you need to split Is... into As... and Bs... down the middle and recurse that way.

    Here is a log-depth split of an index sequence:

    template<class A, class B>
    struct two_things {
      A a;
      B b;
    };
    template<class A, class B>
    two_things(A,B)->two_things<A,B>;
        
    template<class Seq>
    constexpr auto split_sequence( index_t<0>, Seq seq ) {
      return two_things{ std::index_sequence<>{}, seq };
    }
    
    template<std::size_t I0, std::size_t...Is>
    constexpr auto split_sequence( index_t<1>, std::index_sequence<I0, Is...> seq ) {
      return two_things{ std::index_sequence<I0>{}, std::index_sequence<Is...>{} };
    }
    
    template<std::size_t N, class Seq>
    constexpr auto split_sequence( index_t<N>, Seq seq ) {
      constexpr auto step1 = split_sequence( constexpr_index<N/2>, seq );
      constexpr auto step2 = split_sequence( constexpr_index<N-N/2>, step1.b );
      return two_things{ concatenate(step1.a, step2.a), step2.b };
    }
    
    
    template<std::size_t...Is>
    constexpr auto halve_sequence( std::index_sequence<Is...> seq ) {
      return split( index< (sizeof...(Is)) / 2u >, seq );
    }
    

    (two_things exists as a many-many-many times lighter tuple or pair than the std one).

    That in turn lets you improve filter sequence.

    template<std::size_t I, class Test>
    constexpr auto filter_sequence( std::index_sequence<I> seq, Test test )
    {
      if constexpr ( test(constexpr_index<I>) ) {
        return seq;
      } else {
        return std::index_sequence<>{};
      }
    }
    
    template<std::size_t...Is, class Test>
    constexpr auto filter_sequence( std::index_sequence<Is...> seq, Test test )
    {
      constexpr auto split = halve_sequence( seq );
      constexpr auto head = filter_sequence( split.a, test );
      constexpr auto tail = filter_sequence( split.b, test );
      return concatenate(head, tail);
    }
    

    this version should compile faster and use less memory, especially for large numbers of elements. But you should start with the simpler one above, because (as I noted) there are probably plenty of tpyos.

    Live example.