4

I need sometimes to filter some types from a given parameter pack and get the result as a std::tuple. For instance, it can be a filter based on the index of the types in the pack.

As an example, here is a recursive implementation of a stride-like filter, with a semantic similar to std::views::stride

#include <tuple>

namespace pack
{
    template<int R, int IDX, typename...ARGS>
    struct stride_impl  { };

    template<int R, int IDX, typename...ARGS>
    using stride_impl_t = typename stride_impl<R,IDX,ARGS...>::type;

    template<int R, int IDX, typename T, typename...ARGS>
    struct stride_impl<R,IDX,T,ARGS...>
    {
        using type = std::conditional_t <
            IDX%R==0,
            decltype(std::tuple_cat (
                std::tuple<T> {}, 
                stride_impl_t<R,IDX+1,ARGS...> {} 
            )),
            stride_impl_t<R,IDX+1,ARGS...>
        >;     
    };

    template<int R, int IDX>
    struct stride_impl<R,IDX>  {  using type = std::tuple<>;  };

    template<int R, typename...ARGS>
    struct stride 
    {  
        static_assert(R>0);
        using type = stride_impl_t<R,0,ARGS...>;
    };

    template<int R, typename ...ARGS>
    using stride_t = typename stride<R,ARGS...>::type;
};

template<int R> using example = pack::stride_t<R,char,int,long,float,double>;

static_assert (std::is_same_v< example<1>, std::tuple<char,int,long,float,double>>);
static_assert (std::is_same_v< example<2>, std::tuple<char,    long,      double>>);
static_assert (std::is_same_v< example<3>, std::tuple<char,         float       >>);
static_assert (std::is_same_v< example<4>, std::tuple<char,               double>>);
static_assert (std::is_same_v< example<5>, std::tuple<char                      >>);

int main() {}

which feels tedious.

Is it possible to have a more direct implementation that avoids explicit recursion (maybe with fold expression)? Perhaps there is something in std that covers such filters on parameters packs in a similar way to std::views?

3 Answers 3

4

With std::index_sequence, you might do

namespace pack
{

    template<int R, typename...ARGS>
    struct stride 
    {  
        static_assert(R>0);
        using type = decltype([]<std::size_t... Is>(std::index_sequence<Is...>) ->
            std::tuple<std::tuple_element_t<Is * R, std::tuple<ARGS...>>...>
        {
            throw 42;
        }(std::make_index_sequence<std::min(sizeof...(ARGS), 1 + (sizeof...(ARGS) - 1) / R)>()));
    };

    template<int R, typename ...ARGS>
    using stride_t = typename stride<R,ARGS...>::type;
};

Demo

Sign up to request clarification or add additional context in comments.

3 Comments

Is throw 42; really needed ? It seems to compile without it.
@edrezen the function must either return or throw. I suppose return {}; would have worked as well. Note that it isnt actually called.
@463035818_is_not_an_ai: not sure what happen if any ARGS is not default constructible with return {};
0

I eventually found a way to write a pack::view structure that should generalize to other views than stride. It makes it possible to write something like:

using foo = pack::view_t <std::views::stride(3),char,int,long,float,double>;

where we explicitly use std::views::stride as a non type template parameter. It is also possible to compose views:

using drop_take_reverse = typename pack::view_t <
    std::views::drop(1) 
  | std::views::take(3) 
  | std::views::reverse, 
  char,int,long,float,double
>;

static_assert (std::is_same_v<drop_take_reverse, std::tuple<float,long,int>>);

The idea is to create an constexpr std::array from the view representing the indexes to be kept and then "transform" the content of the array into a parameter pack (see here for a solution) used for filtering the required types.

#include <tuple>
#include <array>
#include <ranges>

//////////////////////////////////////////////////////////////////////////////////////////
// see https://stackoverflow.com/questions/60434033/how-do-i-expand-a-compile-time-stdarray-into-a-parameter-pack

template <auto arr, template <auto...> typename Consumer, typename IS = decltype(std::make_index_sequence<arr.size()>())>
struct Generator;

template <auto arr, template <auto...> typename Consumer, std::size_t... I>
struct Generator<arr, Consumer, std::index_sequence<I...>> 
{
    using type = Consumer<arr[I]...>;
};

//////////////////////////////////////////////////////////////////////////////////////////
namespace pack
{
    template<auto View, typename...ARGS>
    struct view
    {
        // we create a std::array holding the indexes to be kept  (no std::ranges::to<std::array<,>> apparently) 
        constexpr static auto indexes_as_array()
        {
            // we define the indexes to be kept in the parameters pack.
            constexpr auto indexes_view =  std::views::iota (size_t{0}, sizeof...(ARGS)) | View ;
        
            std::array<int,indexes_view.size()> arr;
            for (auto [i,x] : indexes_view | std::views::enumerate)  { arr[i]=x; }
            return arr;
        }

        template <auto... s> 
        struct ConsumerStruct 
        {
            using type = std::tuple< std::tuple_element_t<s, std::tuple<ARGS...> >...>;
        };
        
        // we define our type as an application of the Generator with our ConsumerStruct. 
        using type = typename Generator<indexes_as_array(), ConsumerStruct>::type::type;
    }; 
    
    template<auto View, typename ...ARGS> using view_t = typename view<View,ARGS...>::type;
}

//////////////////////////////////////////////////////////////////////////////////////////
template<int R> 
using stride = typename pack::view_t <std::views::stride(R),char,int,long,float,double>;

static_assert (std::is_same_v< stride<1>, std::tuple<char,int,long,float,double>>);
static_assert (std::is_same_v< stride<2>, std::tuple<char,    long,      double>>);
static_assert (std::is_same_v< stride<3>, std::tuple<char,         float       >>);
static_assert (std::is_same_v< stride<4>, std::tuple<char,               double>>);
static_assert (std::is_same_v< stride<5>, std::tuple<char                      >>);

//////////////////////////////////////////////////////////////////////////////////////////
template<int R> 
using drop = typename pack::view_t <std::views::drop(R),char,int,long,float,double>;

static_assert (std::is_same_v< drop<0>, std::tuple<char,int,long,float,double>>);
static_assert (std::is_same_v< drop<1>, std::tuple<     int,long,float,double>>);
static_assert (std::is_same_v< drop<2>, std::tuple<         long,float,double>>);
static_assert (std::is_same_v< drop<3>, std::tuple<              float,double>>);
static_assert (std::is_same_v< drop<4>, std::tuple<                    double>>);
static_assert (std::is_same_v< drop<5>, std::tuple<                          >>);

//////////////////////////////////////////////////////////////////////////////////////////
template<int R> 
using take = typename pack::view_t <std::views::take(R),char,int,long,float,double>;

static_assert (std::is_same_v< take<5>, std::tuple<char,int,long,float,double>>);
static_assert (std::is_same_v< take<4>, std::tuple<char,int,long,float       >>);
static_assert (std::is_same_v< take<3>, std::tuple<char,int,long             >>);
static_assert (std::is_same_v< take<2>, std::tuple<char,int                  >>);
static_assert (std::is_same_v< take<1>, std::tuple<char                      >>);
static_assert (std::is_same_v< take<0>, std::tuple<                          >>);

//////////////////////////////////////////////////////////////////////////////////////////
template<int A, int B> 
using drop_take_reverse = typename pack::view_t <std::views::drop(A) | std::views::take(B) | std::views::reverse, char,int,long,float,double>;

static_assert (std::is_same_v<drop_take_reverse<1,3>, std::tuple<float,long,int>>);

//////////////////////////////////////////////////////////////////////////////////////////
int main() {}

Demo

A limitation is that we need to call size one the view in order to know the size of the array to be created, so views like std::views::filter can't be used in this context.


UPDATE

We can go beyong this limitation with a trick: we compute an array of size sizeof...(ARGS) and fill it with the items of the view. We also remember the actual number of items of the view => it is used later in order to shrink the indexes array to the correct size.
Demo

So we can also write something like:

using filtereven = pack::view_t<std::views::filter([](int n) {return n%2==0;}), char,int,long,float,double>;
static_assert (std::is_same_v<filtereven, std::tuple<char,long,double>>);

Comments

0

First, generate a sample system:

template<class K, class T>
struct kv_t {
  using type=T;
};
template<class...KVs>
struct kv_ts: KVs... {
  template<class K, class V>
  constexpr static kv_t<K, V> kv_helper(kv_t<K, V>) { return {}; }
  template<class K>
  constexpr static auto lookup() { return kv_helper<K>(kv_ts{}); }

  template<class K>
  using type = typename decltype( lookup<K>() )::type;
};
template<std::size_t I>
using index_t=std::integral_constant<std::size_t, I>;

template<class Z, class Is, class Ks=void>
struct sample;
template<class Z, class Is>
using sample_t = typename sample<Z, Is>::type;

template<template<class...>class Z, class...Ts, class Is>
struct sample< Z<Ts...>, Is, void >:
  sample< Z<Ts...>, Is, std::make_index_sequence<sizeof...(Ts)> >
{};

template<template<class...>class Z, class...Ts, std::size_t...Is, std::size_t...Ks>
struct sample< Z<Ts...>, std::index_sequence<Is...>, std::index_sequence<Ks...> >
{
  using type = Z< typename kv_ts< kv_t< index_t<Ks>, Ts >... >::template type<index_t<Is>>... >;
};

now, sample_t< std::tuple<int, double, char>, std::index_sequence<1,2> > is std::tuple<double, char>.

I could use std::tuple for some of this, but I find it is overly bulky and it compiles to too much code. Instead I wrote my own key-value lookup thing that does nothing but do that lookup.

Now if you want to stride:

template<std::size_t S, std::size_t C>
constexpr auto stride_sequence() {
  using base = std::make_index_sequence<C>;
  return []<std::size_t...Is>(std::index_sequence<Is...>){
    return std::index_sequence<(Is*S)...>{};
  }(base{});
}

template<std::size_t S, std::size_t C>
using stride_sequence_t = decltype( stride_sequence<S,C>() );

we can now sample a test tuple:

using base_tuple = std::tuple<char, double, int, std::string, void*>;

template<std::size_t stride>
using example_t = sample_t< base_tuple, stride_sequence_t<stride, (std::tuple_size_v<base_tuple>+stride-1) / stride>;

...

I'd be tempted to do this generically.

Take Z<Ts...> -> Z< Q<Ts>... >, where Q<Ts> can be Z<???>. In a basic filter, it is Z<> for "drop" and Z<Ts> for "keep". Then flatten 1 step. This is the fmap/bind style.

Each type gets mapped to 0 or more types, then the results are concatenated.

To make this work with purely positional filtering is a bit annoying, because written naturally you don't get the positional information. You can do this:

Z<Ts...> -> Z< (Is, Ts)... >, then Z< Q<(Is, Ts)>... >, then back, then drop the Is. Ie, turn each type into an index/element pair, do work on the index/element pairs, then collapse and remove the indexes.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.