Search code examples
c++tuplestemplate-meta-programming

Filter a tuple based on a constexpr lambda


I want to take a tuple and look through its elements via a constexpr lambda, for each element that the lambda returns true, it is accepted into a new tuple and returned. The function in question is filter_tuple and it should be constexpr with that exact call signature. Here's what I have so far:

Demo:

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

template <typename T>
struct is_tuple : std::false_type {};

template <typename... Args>
struct is_tuple<std::tuple<Args...>> : std::true_type {};

template <typename T>
static constexpr bool is_tuple_v = is_tuple<T>::value;


template <typename T, typename Fn>
constexpr auto check(const T& val, Fn&& fn) -> typename std::conditional<fn(val), std::tuple<>, std::tuple<T>>::type {
    if constexpr(fn(val)) {
        return std::make_tuple(val);
    } else {
        return std::make_tuple();
    }
}

template <typename T, typename Fn> requires (is_tuple_v<T>)
constexpr auto filter_tuple(const T& tpl, Fn&& filter_fn) {
    return [&]<std::size_t... Is>(std::index_sequence<Is...>){
        std::tuple_cat(check(std::get<Is>(tpl), filter_fn)...);
    }(std::make_index_sequence<std::tuple_size_v<T>>{});
}

template <class Tuple, typename F>
inline constexpr decltype(auto) for_each_in_tuple(Tuple&& tuple, F&& f) {
    return [] <std::size_t... I>
    (Tuple&& tuple, F&& f, std::index_sequence<I...>)
    {
        (f(std::get<I>(tuple)), ...);
        return f;
    }(std::forward<Tuple>(tuple), std::forward<F>(f),
    std::make_index_sequence<std::tuple_size<std::remove_reference_t<Tuple>>::value>{});
}


auto tpl = std::make_tuple(
        [](auto a, auto b) { return a + b; },
        10,
        25,
        42.42,
        "Hello",
        std::string("there")
);

auto tpl_type_filtered = filter_tuple(tpl, []<typename T>(const T&) constexpr -> bool { return std::is_integral_v<T>; } );

int main() {
    // // "tpl_type_filtered" = 10 25

    for_each_in_tuple(tpl_type_filtered, [](auto& val){ std::cout << val << std::endl; });
}

However it fails to complile seemingly because the lambda invocation is not accepted as constexpr. It doesn't really get more constexpr than literally declaring it so, so how can I enforce this? Those are the errors:

<source>:18:109: error: template argument 1 is invalid
   18 | constexpr auto check(const T& val, Fn&& fn) -> typename std::conditional<fn(val), std::tuple<>, std::tuple<T>>::type {
      |                                                                                                             ^~
<source>:18:109: error: template argument 1 is invalid
<source>:18:62: error: invalid use of template-name 'std::conditional' without an argument list
   18 | constexpr auto check(const T& val, Fn&& fn) -> typename std::conditional<fn(val), std::tuple<>, std::tuple<T>>::type {
      |                                                              ^~~~~~~~~~~
In file included from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/move.h:57,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/exception_ptr.h:41,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/exception:164,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/ios:41,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/ostream:40,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/iostream:41,
                 from <source>:1:
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/type_traits:2235:12: note: 'template<bool _Cond, class _Iftrue, class _Iffalse> struct std::conditional' declared here
 2235 |     struct conditional
      |            ^~~~~~~~~~~
<source>:18:73: error: expected initializer before '<' token
   18 | constexpr auto check(const T& val, Fn&& fn) -> typename std::conditional<fn(val), std::tuple<>, std::tuple<T>>::type {
      |                                                                         ^
<source>: In instantiation of 'filter_tuple<std::tuple<<lambda(auto:16, auto:17)>, int, int, double, const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, <lambda(const T&)> >(const std::tuple<<lambda(auto:16, auto:17)>, int, int, double, const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&, <lambda(const T&)>&&)::<lambda(std::index_sequence<Is ...>)> [with long unsigned int ...Is = {0, 1, 2, 3, 4, 5}; std::index_sequence<Is ...> = std::integer_sequence<long unsigned int, 0, 1, 2, 3, 4, 5>]':
<source>:30:6:   required from 'constexpr auto filter_tuple(const T&, Fn&&) [with T = std::tuple<<lambda(auto:16, auto:17)>, int, int, double, const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >; Fn = <lambda(const T&)>]'
<source>:54:38:   required from here
<source>:29:29: error: 'check' was not declared in this scope
   29 |         std::tuple_cat(check(std::get<Is>(tpl), filter_fn)...);
      |                        ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:54:6: error: deduced type 'void' for 'tpl_type_filtered' is incomplete
   54 | auto tpl_type_filtered = filter_tuple(tpl, []<typename T>(const T&) constexpr -> bool { return std::is_integral_v<T>; } );
      |      ^~~~~~~~~~~~~~~~~
ASM generation compiler returned: 1
<source>:18:109: error: template argument 1 is invalid
   18 | constexpr auto check(const T& val, Fn&& fn) -> typename std::conditional<fn(val), std::tuple<>, std::tuple<T>>::type {
      |                                                                                                             ^~
<source>:18:109: error: template argument 1 is invalid
<source>:18:62: error: invalid use of template-name 'std::conditional' without an argument list
   18 | constexpr auto check(const T& val, Fn&& fn) -> typename std::conditional<fn(val), std::tuple<>, std::tuple<T>>::type {
      |                                                              ^~~~~~~~~~~
In file included from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/move.h:57,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/exception_ptr.h:41,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/exception:164,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/ios:41,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/ostream:40,
                 from /opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/iostream:41,
                 from <source>:1:
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/type_traits:2235:12: note: 'template<bool _Cond, class _Iftrue, class _Iffalse> struct std::conditional' declared here
 2235 |     struct conditional
      |            ^~~~~~~~~~~
<source>:18:73: error: expected initializer before '<' token
   18 | constexpr auto check(const T& val, Fn&& fn) -> typename std::conditional<fn(val), std::tuple<>, std::tuple<T>>::type {
      |                                                                         ^
<source>: In instantiation of 'filter_tuple<std::tuple<<lambda(auto:16, auto:17)>, int, int, double, const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, <lambda(const T&)> >(const std::tuple<<lambda(auto:16, auto:17)>, int, int, double, const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&, <lambda(const T&)>&&)::<lambda(std::index_sequence<Is ...>)> [with long unsigned int ...Is = {0, 1, 2, 3, 4, 5}; std::index_sequence<Is ...> = std::integer_sequence<long unsigned int, 0, 1, 2, 3, 4, 5>]':
<source>:30:6:   required from 'constexpr auto filter_tuple(const T&, Fn&&) [with T = std::tuple<<lambda(auto:16, auto:17)>, int, int, double, const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >; Fn = <lambda(const T&)>]'
<source>:54:38:   required from here
<source>:29:29: error: 'check' was not declared in this scope
   29 |         std::tuple_cat(check(std::get<Is>(tpl), filter_fn)...);
      |                        ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:54:6: error: deduced type 'void' for 'tpl_type_filtered' is incomplete
   54 | auto tpl_type_filtered = filter_tuple(tpl, []<typename T>(const T&) constexpr -> bool { return std::is_integral_v<T>; } );
      |      ^~~~~~~~~~~~~~~~~

Solution

  • You can make these changes for this to work:

    The lambda needs to be passed by-value. Since the operator() won't use the value of *this, it can then be used in a constant expression.

    Passing the tuple by parameter is a lost cause if you want to filter based on their value. But since you are only filtering the types of the expressions, you can just pass the type to the lambda:

    template <typename T, typename Fn>
    constexpr auto check(const T& val, Fn fn) {
        if constexpr(fn.template operator()<T>()) {
            return std::make_tuple(val);
        } else {
            return std::make_tuple();
        }
    }
    
    auto tpl_type_filtered = filter_tuple(tpl, []<typename T>() constexpr -> bool { return std::is_integral_v<T>; } );
    

    Though you can write your function in a way to avoid make_tuple + tuple_cat (two copies):

    template<typename Tuple, typename Fn>
    constexpr auto filter_tuple(Tuple&& tpl, Fn filter_fn) {
        using tuple = std::remove_cvref_t<Tuple>;
        return [&]<std::size_t... I>(std::index_sequence<I...>) {
            constexpr auto filtered = [&]{
                std::array<std::size_t, sizeof...(I)> filtered_indices;
                std::size_t* result_ptr = filtered_indices.data();
                ([&]{
                    // If it passes the filter, write its index to std::get later
                    if constexpr (filter_fn.template operator()<std::tuple_element_t<I, tuple>>())
                        *result_ptr++ = I;
                }(), ...);
                std::size_t filtered_size = result_ptr - filtered_indices.data();
                return std::pair{filtered_indices, filtered_size};
            }();
            constexpr auto filtered_indices = filtered.first;
            constexpr auto filtered_size = filtered.second;
            // std::get all the indices that passed the filter
            return [&]<std::size_t... J>(std::index_sequence<J...>) {
                return std::make_tuple(std::get<filtered_indices[J]>(std::forward<Tuple>(tpl))...);
            }(std::make_index_sequence<filtered_size>{});
        }(std::make_index_sequence<std::tuple_size_v<tuple>>{});
    }
    
    constexpr auto tpl_type_filtered = filter_tuple(tpl, []<typename T>() {
        return std::is_integral_v<T>;
    });
    // tpl_type_filtered == std::tuple<int, int>{ 10, 25 };
    

    And if you wanted to do it based on the value of the tuple, you have to pass in the tuple another way. Easiest is to pass a lambda returning the tuple instead:

    template<typename TupleFactory, typename Fn>
    constexpr auto filter_tuple(TupleFactory tpl_factory, Fn filter_fn) {
        constexpr decltype(auto) tpl = tpl_factory();
        using tuple = std::remove_cvref_t<decltype(tpl)>;
        return [&]<std::size_t... I>(std::index_sequence<I...>) {
            constexpr auto filtered = [&]{
                std::array<std::size_t, sizeof...(I)> filtered_indices;
                std::size_t* result_ptr = filtered_indices.data();
                ([&]{
                    if constexpr (filter_fn(std::get<I>(tpl)))
                        *result_ptr++ = I;
                }(), ...);
                std::size_t filtered_size = result_ptr - filtered_indices.data();
                return std::pair{filtered_indices, filtered_size};
            }();
            constexpr auto filtered_indices = filtered.first;
            constexpr auto filtered_size = filtered.second;
            return [&]<std::size_t... J>(std::index_sequence<J...>) {
                return std::make_tuple(std::get<filtered_indices[J]>(tpl)...);
            }(std::make_index_sequence<filtered_size>{});
        }(std::make_index_sequence<std::tuple_size_v<tuple>>{});
    }
    
    constexpr auto tpl = std::make_tuple(
            [](auto a, auto b) { return a + b; },
            10,
            25,
            42.42,
            "Hello"
    );
    
    // Keep all arithmetic values greater than 15
    constexpr auto tpl_type_filtered = filter_tuple([]{ return tpl; }, []<typename T>(const T& value) {
        if constexpr (std::is_arithmetic_v<T>) {
            return value > 15;
        } else {
            return false;
        }
    });
    
    static_assert(tpl_type_filtered == std::tuple<int, double>{ 25, 42.42 });