AlecadM
AlecadM

Reputation: 3

Loop over pairs from 2 constexpr arrays to instantiate and run template function

C++ version for project is 20.

Suppose we have some service class template:

template<int someSegmentSize>
class SomeAbstractFileService {
public:
    void init() {
        std::cout << "Initializing FileService with segment size: " << someSegmentSize << std::endl;
    }

    template<int HashNum>
    void create_shemas() {
        std::cout << "Creating  with hash function number: " << HashNum << " and segment size: " << someSegmentSize << std::endl;
    }
    //some other non template functions ...
};

and 2 constexpr arrays(currently filled with arbitrary values):

constexpr size_t N=7;
constexpr size_t N2=7;//sizes may differ
constexpr std::array<int,N> segment_size_arr{-1, -2, -3, -4, -5, -6, -7};
constexpr std::array<int,N2> hash_num_arr{1, 2, 3, 4, 5, 6, 7};

For testing purposes i want to iterate through all pairs of values of both arrays (like (-1,1),(-1,2),...,(-1,7),(-2,1),...,(-7,7)) to instantiate and call this function template:

template<int SegmentSize,int Hash>
void performStuff()
{
    std::cout<<SegmentSize<<'\t'<<Hash<<'\n';
    SomeAbstractFileService<SegmentSize> ss;
    ss.init();
    ss.template create_shemas<Hash>();//this function must be called with new object of SomeAbstractFileService otherwise schema update will collide with existing one
}

This is one of my attempts using folds and lambda expressions(inspired by solution):

#include<iostream>
#include<array>


template<int someSegmentSize>
class SomeAbstractFileService {
public:
    void init() {
        std::cout << "Initializing FileService with segment size: " << someSegmentSize << std::endl;
    }

    template<int HashNum>
    void create_shemas() {
        std::cout << "Creating  with hash function number: " << HashNum << " and segment size: " << someSegmentSize << std::endl;
    }
    //some other non template functions ...
};



template<int SegmentSize,int Hash>
void performStuff()
{
    std::cout<<SegmentSize<<'\t'<<Hash<<'\n';
    SomeAbstractFileService<SegmentSize> ss;
    ss.init();
    ss.template create_shemas<Hash>();

}

template<typename T,size_t N,size_t N2,const std::array<T,N> segment_size_arr,const std::array<T,N2> hash_num_arr>
constexpr void iterate_pairs() {
    []<std::size_t... Is>(std::index_sequence<Is...> is) {

        (([]<std::size_t val,std::size_t... Is2>(std::index_sequence<Is...> is,std::index_sequence<Is2...> is2)
        {
            constexpr T segsize=segment_size_arr[val];//some caveman check
            (performStuff<segment_size_arr[val],hash_num_arr[Is2]>(),...);


        }<Is>(is,std::make_index_sequence<N2>{})),...);

    }(std::make_index_sequence<N>{});
}



int main(){
    constexpr size_t N=7;
    constexpr size_t N2=7;//sizes may differ
    constexpr std::array<int,N> segment_size_arr{-1, -2, -3, -4, -5, -6, -7};//actually may be global variable
    constexpr std::array<int,N2> hash_num_arr{1, 2, 3, 4, 5, 6, 7};//actually may be global variable

    iterate_pairs<int, N, N2, segment_size_arr, hash_num_arr>();

}

but this one results in an error:

Invalid operands to binary expression ('(lambda at /.../main.cpp:39:11)' and 'unsigned long') main2.cpp:35:5: note: in instantiation of function template specialization 'iterate_pairs()::(anonymous class)::operator()<0UL, 1UL, 2UL, 3UL, 4UL, 5UL, 6UL>' requested here main2.cpp:58:5: note: in instantiation of function template specialization 'iterate_pairs<int, 7UL, 7UL, std::array<int, 7UL>{{-1, -2, -3, -4, -5, -6, -7}}, std::array<int, 7UL>{{1, 2, 3, 4, 5, 6, 7}}>' requested here

What will be a proper way to perform such iteration? Can this one be generalized for arbitrary number of constexpr arrays(all triplets, quads ...)?

Update

After some time and practice created my "frankenstein" code from answers by users edrezen and Kirisame Igna.

Code

Upvotes: 0

Views: 139

Answers (3)

edrezen
edrezen

Reputation: 1626

As an extension of my previous answer, it is possible to consider that each item to be iterated is just a point in a tensor whose K dimensions are provided by the dimensions of each input array.

Since we know the number of items in the tensor (the product N of the dimension of each array), we can construct an array holding all the items of the tensor, which in the end is the wanted output.

Indeed, one can iterate from 1 to N and then retrieve the corresponding K indexes in the tensor and we are done.

In other words, we can compute the Cartesian product by a two-loops iteration. Proceeding this way may require less metaprogramming complex code than other approaches.

#include <tuple>
#include <array>
#include <type_traits>

////////////////////////////////////////////////////////////////////////////////
template<typename...ARRAYS>
constexpr auto cartesian_product (ARRAYS...arrays)
{
    // we define the type of an entry
    using type = std::tuple<typename ARRAYS::value_type...>;

    // we define the number of entries
    constexpr std::size_t N = (1 * ... * arrays.size());

    // we compute the dimensions of the successive parts of the tensor
    std::array<std::size_t,sizeof...(arrays)> dims { arrays.size()... };
    for (std::size_t i=1; i<dims.size(); ++i)  { dims[i] *= dims[i-1]; }
        
    return [&] () 
    {
        // the result is an array holding N tuples.
        std::array<type, N> result;

        // we iterate each entry of the result array.
        for (std::size_t i=0; i<result.size(); ++i)
        {
            [&]<std::size_t... Is>(std::index_sequence<Is...>) 
            {
               // we demultiplex the current index for each array from the global index 'i'
               auto idx = std::make_tuple ( ( (i*std::get<Is>(dims)) / N) % arrays.size() ...);
   
               // we set the required values for the current entry.             
               result[i] = std::make_tuple (arrays[std::get<Is>(idx)]...);
            
            }(std::make_index_sequence<sizeof...(arrays)>{});
        }

        return result;
    }();
}

////////////////////////////////////////////////////////////////////////////////
constexpr std::array<int,   3> a1  {1, 2, 3}; 
constexpr std::array<char,  2> a2  {'a','b'}; 
constexpr std::array<double,4> a3  {2.0, 4.0, 6.0, 8.0}; 

constexpr auto entries = cartesian_product (a1,a2,a3);

static_assert (entries.size()==24);
static_assert (std::is_same_v<decltype(entries)::value_type, std::tuple<int,char,double>>);

static_assert (entries[ 0] == std::make_tuple (1, 'a', 2.0) );
static_assert (entries[ 1] == std::make_tuple (1, 'a', 4.0) );
static_assert (entries[ 2] == std::make_tuple (1, 'a', 6.0) );
static_assert (entries[ 3] == std::make_tuple (1, 'a', 8.0) );

static_assert (entries[ 4] == std::make_tuple (1, 'b', 2.0) );
static_assert (entries[ 5] == std::make_tuple (1, 'b', 4.0) );
static_assert (entries[ 6] == std::make_tuple (1, 'b', 6.0) );
static_assert (entries[ 7] == std::make_tuple (1, 'b', 8.0) );

static_assert (entries[ 8] == std::make_tuple (2, 'a', 2.0) );
static_assert (entries[ 9] == std::make_tuple (2, 'a', 4.0) );
static_assert (entries[10] == std::make_tuple (2, 'a', 6.0) );
static_assert (entries[11] == std::make_tuple (2, 'a', 8.0) );

static_assert (entries[12] == std::make_tuple (2, 'b', 2.0) );
static_assert (entries[13] == std::make_tuple (2, 'b', 4.0) );
static_assert (entries[14] == std::make_tuple (2, 'b', 6.0) );
static_assert (entries[15] == std::make_tuple (2, 'b', 8.0) );

static_assert (entries[16] == std::make_tuple (3, 'a', 2.0) );
static_assert (entries[17] == std::make_tuple (3, 'a', 4.0) );
static_assert (entries[18] == std::make_tuple (3, 'a', 6.0) );
static_assert (entries[19] == std::make_tuple (3, 'a', 8.0) );

static_assert (entries[20] == std::make_tuple (3, 'b', 2.0) );
static_assert (entries[21] == std::make_tuple (3, 'b', 4.0) );
static_assert (entries[22] == std::make_tuple (3, 'b', 6.0) );
static_assert (entries[23] == std::make_tuple (3, 'b', 8.0) );

int main() {}

Demo

Upvotes: 0

Kirisame Igna
Kirisame Igna

Reputation: 401

First of all, about your questions.

  1. Your way is the proper way of doing this AFAIK, though missing one single part; see next section.

  2. To do this (compile-time cartesian product) on arbitrary number of constexpr arrays, you can utilize a simple recursion, and tons of template metaprogramming shit:

#include <fmt/format.h>
#include <fmt/ranges.h>

#include <array>
#include <functional>
#include <iostream>

template <auto Val>
struct val_wrapper {
    static constexpr auto value = Val;
};

template <class... T>
struct type_list;

template <class First, class... Rest>
struct type_list<First, Rest...> {
    using head = First;
    using tail = type_list<Rest...>;

    template <class T>
    using append = type_list<First, Rest..., T>;

    static constexpr bool empty = false;
};

template <>
struct type_list<> {
    template <class T>
    using append = type_list<T>;

    static constexpr bool empty = true;
};

template <auto Func, auto HeadArr, typename TailArr, typename HistoryVals>
constexpr void product_impl() {
    if constexpr (TailArr::empty) {
        constexpr size_t len_of_current = HeadArr.size();

        []<size_t... Idx>(std::index_sequence<Idx...>) {
            constexpr auto with_i = []<size_t I> {
                using current_vals = typename HistoryVals::template append<
                    val_wrapper<HeadArr[I]>>;

                []<auto... Vals>(type_list<val_wrapper<Vals>...>) {
                    std::invoke(Func, Vals...);
                }(current_vals{});
            };
            ((with_i.template operator()<Idx>()), ...);
        }(std::make_index_sequence<len_of_current>{});
    } else {
        constexpr size_t len_of_current = HeadArr.size();
        []<size_t... Idx>(std::index_sequence<Idx...>) {
            (product_impl<Func, TailArr::head::value, typename TailArr::tail,
                          typename HistoryVals::template append<
                              val_wrapper<HeadArr[Idx]>>>(),
             ...);
        }(std::make_index_sequence<len_of_current>{});
    }
}

template <auto Func, auto... Arrs>
constexpr void product() {
    if constexpr (sizeof...(Arrs) == 0) {
        return;
    } else {
        []<auto HeadArr, auto... TailArr> {
            product_impl<Func, HeadArr, type_list<val_wrapper<TailArr>...>,
                         type_list<>>();
        }.template operator()<Arrs...>();
    }
}

int main() {
    constexpr std::array v1{-1, -2};
    constexpr std::array v2{1, 2};
    constexpr std::array v3{3, 4};

    constexpr auto print_all = [](auto... values) {
        fmt::println("[{}]", fmt::join({values...}, ", "));
    };

    product<print_all, v1, v2, v3>();
}

https://godbolt.org/z/WE5bze9db

I used fmt for demonstration. I'd very like to explain how this works, but it's already late enough for me to worry about my health here, so I guess I have to write it later even if you're interested in it. Sorry 'bout that.


And then, about the error: I formatted your code, and things immediately became clear.

template <typename T, size_t N, size_t N2,
          const std::array<T, N> segment_size_arr,
          const std::array<T, N2> hash_num_arr>
constexpr void iterate_pairs() {
    []<std::size_t... Is>(std::index_sequence<Is...> is) {
        (([]<std::size_t val, std::size_t... Is2>(
              std::index_sequence<Is...> is, std::index_sequence<Is2...> is2) {
             constexpr T segsize = segment_size_arr[val];
             (performStuff<segment_size_arr[val], hash_num_arr[Is2]>(), ...);
         } < Is > (is, std::make_index_sequence<N2>{})), // noticed this line?
         ...);
    }(std::make_index_sequence<N>{});
}

The lambda itself is not a function (and thus not a template function either), but rather a object of a anonymous class, with a operator() that has the same signature as the lambda's, and has the same implementation as the lambda's body; this is the reason why lambdas are sometimes called as a kind of "syntactic sugar".

This gives the lambda object the ability to be called like a normal function; sometimes it is called a "functor" in the C++ world. In a templated lambda's case, the real templated thing is the operator() on it, not the lambda "itself", so here the compiler thinks you are comparing the lambda and Is. The correct way of writing this would be explicitly write out the operator(), using the normal syntax to call a templated member function:

template <typename T, size_t N, size_t N2,
          const std::array<T, N> segment_size_arr,
          const std::array<T, N2> hash_num_arr>
constexpr void iterate_pairs() {
    []<std::size_t... Is>(std::index_sequence<Is...> is) {
        (([]<std::size_t val, std::size_t... Is2>(
              std::index_sequence<Is...> is, std::index_sequence<Is2...> is2) {
             constexpr T segsize = segment_size_arr[val];
             (performStuff<segment_size_arr[val], hash_num_arr[Is2]>(), ...);
         }.template operator()<Is>(is, std::make_index_sequence<N2>{})), // like this
         ...);
    }(std::make_index_sequence<N>{});
}

The weird template prefix is to tell the compiler that the dependent name operator() is a template, for disambiguation. Check cppreference for more details, though it seems like you already know this.

Upvotes: 1

edrezen
edrezen

Reputation: 1626

Here is a possible solution. I don't think that iterate_pairs needs two lambdas, one is enough to get the index of each item to be used for the two arrays if you accept to deal with an index that encapsulates the two required indexes

#include<iostream>
#include<array>
#include<iostream>


template<int someSegmentSize>
class SomeAbstractFileService {
public:
    void init() {
        std::cout << "Initializing FileService with segment size: " << someSegmentSize << std::endl;
    }

    template<int HashNum>
    void create_shemas() {
        std::cout << "Creating  with hash function number: " << HashNum << " and segment size: " << someSegmentSize << std::endl;
    }
    //some other non template functions ...
};



template<int SegmentSize,int Hash>
void performStuff()
{
    std::cout<<SegmentSize<<'\t'<<Hash<<'\n';
    SomeAbstractFileService<SegmentSize> ss;
    ss.init();
    ss.template create_shemas<Hash>();

}

template<typename T,size_t N,size_t N2,const std::array<T,N> segment_size_arr,const std::array<T,N2> hash_num_arr>
constexpr void iterate_pairs() 
{
    static_assert (N>0 && N2>0);
    []<std::size_t... Is>(std::index_sequence<Is...>) 
    {
        (performStuff<segment_size_arr[Is/N], hash_num_arr[Is%N2]>(),...);
    }(std::make_index_sequence<N*N2>{});
}

int main(){
    constexpr size_t N=7;
    constexpr size_t N2=7;//sizes may differ
    
    constexpr std::array<int,N> segment_size_arr{-1, -2, -3, -4, -5, -6, -7}; //actually may be global variable
    constexpr std::array<int,N2> hash_num_arr   { 1,  2,  3,  4,  5,  6,  7}; //actually may be global variable

    iterate_pairs<int, N, N2, segment_size_arr, hash_num_arr>();

}

Obviously, it can't be easily extended if one needs to use more than two entries.

Demo

Upvotes: 0

Related Questions