lightxbulb
lightxbulb

Reputation: 1321

Distribute non-type parameter pack across different template parameter packs

Is there any syntax through which I can distribute a non-type parameter pack across the parameters of a parameter pack of templates, expecting non-type packs (of different sizes)? Since this is pretty confusing I believe that an example may help to clarify what I mean: https://godbolt.org/z/FaEGTV

template <typename T, int... I> struct Vec { };

struct A
{
    template<template<typename, int...> typename...  Container,
        typename... Ts, int... Is>
    A(Container<Ts,Is...>... );
};  

A a(Vec<int, 0>{}, Vec<double, 0>{});       // ok
A b(Vec<int, 0, 1>{}, Vec<double, 0, 1>{}); // ok
A c(Vec<int, 0>{}, Vec<double, 0, 1>{});    // error

I want the line marked // error to work with a syntax similar to what I have. Clearly it will work fine if I write a special constructor to handle this case. However I want this to work for any number of containers, without me having to spell it out explicitly for all possible cases. For example, if I have 2 containers a,b, with index sets {0,1,2} and {0,1,2,3} the expansion should look like A(a[0],a[1],a[2], b[0],b[1],b[2],b[3]).

I am aware that I could do this recursively, unpacking one container at a time, and delegating recursively to constructors expecting a sequence of only flat elements in the beginning. My question is whether this is feasible in a more elegant, efficient, and less verbose way.

Upvotes: 3

Views: 293

Answers (3)

Jarod42
Jarod42

Reputation: 217338

I would simply do:

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

template<template<typename, size_t...> typename  C, typename T, size_t... Is>
struct is_container_type<C<T, Is...>> : std::true_type
{
    // Potential `using` to retrieve C, T, Is...
};

struct A
{
    template<typename...  Cs, REQUIRES(is_container_type<Cs>::value && ...)>
    A(Cs... cs)
    {
        ((void)(std::cout << cs << "\n"), ...);
    }
}; 

Demo

With multi_apply (std::apply version for multiple tuple) (you can found there), you might do:

struct A
{

    template<typename...  Ts, REQUIRES(!is_container_type<Ts>::value || ...)>
    A(Ts... ts)
    {
        ((std::cout << ts << " "), ...);
        std::cout << std::endl;
    }


    template<typename...  Cs, REQUIRES(is_container_type<Cs>::value && ...)>
    A(Cs... cs) :
        A(multiple_apply([](auto...args){ return A(args...); },
                         cs...) // if Cs is tuple-like
                         // as_tuple(cs)...) // convert Cs to tuple<int, /*..*/, int>
          )
    {
    }

}; 

Demo

Upvotes: 1

max66
max66

Reputation: 66210

For example, if I have 2 containers a,b, with index sets {0,1,2} and {0,1,2,3} the expansion should look like A(a[0],a[1],a[2], b[0],b[1],b[2],b[3]).

I am aware that I could do this recursively, unpacking one container at a time, and delegating recursively to constructors expecting a sequence of only flat elements in the beginning. My question is whether this is feasible in a more elegant, efficient, and less verbose way.

Do you accept a solution where the expansion give you a std::tuple with a[0],a[1],a[2], b[0],b[1],b[2],b[3] ?

In this case, you can follows the Igor's suggestion, unpack the values in the containers and repack they in tuples and use std::tuple_cat() to concatenate the tuples.

I mean... given a container/tuple converter as follows

template <template<typename, std::size_t...> typename C,
          typename T, std::size_t... Is>
auto getTpl (C<T, Is...> const & v)
 { return std::make_tuple(v.data[Is]...); } 

you can start write your constructor, calling a delegate constructor, as follows

   template <typename ... Ts>
   A (Ts const & ... ts) : A{ std::tuple_cat( getTpl(ts)... ) }
    { } 

and the final constructor is

   template <typename ... Ts>
   A (std::tuple<Ts...> const & tpl)
    { /* do something with values inside tpl */ }

The following is a full compiling example

#include <iostream>
#include <string>
#include <tuple>

template <typename T, std::size_t ... Is>
struct Vec
 {
   T data [sizeof...(Is)] = { Is... };

   T const & operator[] (std::size_t i) const
    { return data[i]; }

   T & operator[] (std::size_t i)
    { return data[i]; }
 };

template <template<typename, std::size_t...> typename C,
          typename T, std::size_t... Is>
auto getTpl (C<T, Is...> const & v)
 { return std::make_tuple(v.data[Is]...); }

struct A
 {
   template <typename ... Ts>
   A (std::tuple<Ts...> const & tpl)
    { /* do something with values inside tpl */ }

   template <typename ... Ts>
   A (Ts const & ... ts) : A{ std::tuple_cat( getTpl(ts)... ) }
    { } 
 };  

int main ()
 {
   A a(Vec<int, 0>{}, Vec<double, 0>{});       // ok
   A b(Vec<int, 0, 1>{}, Vec<double, 0, 1>{}); // ok
   A c(Vec<int, 0>{}, Vec<double, 0, 1>{});    // ok, now
 }

Upvotes: 1

lightxbulb
lightxbulb

Reputation: 1321

I managed to produce a somewhat inefficient solution, that builds a larger vector in the first argument with every recursive step, and then has also a constructor that accepts a single vector and expands that. Clearly this is not ideal, since N vectors are created with a total number of elements N^2/2 in the worst case. Here is an example implementation illustrating what I did: https://coliru.stacked-crooked.com/a/1f41f3793846cdb1

I tried a different version, where no new objects are constructed, however the compiler didn't manage to deduce the proper constructor for some reason (I supposed it's due to the multiple expansions) - this version is in the GROW_VECTOR 0 define in the example linked above.

Upvotes: 0

Related Questions