Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>; } );
      |      ^~~~~~~~~~~~~~~~~
like image 657
glades Avatar asked Oct 15 '25 19:10

glades


2 Answers

You cannot change the return type of a function based on a function parameter.

The way you could pass the filter here to make this work would be by using a template template parameter that refers to a type containing a static constexpr bool member for use as the predicate.

template<template<class> class Filter, class Arg>
constexpr auto TupleFilterSingle(Arg const& arg)
{
    if constexpr (Filter<Arg>::value)
    {
        return std::tuple<Arg>{ arg };
    }
    else
    {
        return std::tuple<>{};
    }
}

template<template<class> class Filter, class ... Args>
constexpr auto TupleFilter(std::tuple<Args...> const& input)
{
    return ([&input]<size_t ...Ints>(std::index_sequence<Ints...>) {
        return std::tuple_cat(TupleFilterSingle<Filter>(std::get<Ints>(input))...);
    })(std::make_index_sequence<sizeof...(Args)>());
}

Usage:

auto tpl_type_filtered = TupleFilter<std::is_integral>(tpl);
like image 147
fabian Avatar answered Oct 18 '25 13:10

fabian


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 });
like image 33
Artyer Avatar answered Oct 18 '25 12:10

Artyer



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!