3

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
lightxbulb
  • 1,251
  • 12
  • 29
  • Huh. I'm surprised even `b` compiles in this example. – Barry Oct 24 '19 at 14:21
  • @Barry Because usually different size expansions do not work together? I think it is because it is restricted to `Container`. – lightxbulb Oct 24 '19 at 14:22
  • 4
    `Container...` says: for each `T` in `Ts`, take a `Container` with that `T` and all the `Is`. That's why all the `Is` must be the same. The easiest way to get your example to compile is `template A(Cs...);` You could then have helper templates to extract `T` and `Is` from each `C` in `Cs` – Igor Tandetnik Oct 24 '19 at 14:30
  • @IgorTandetnik I get what the `Is...` is doing. I was hoping that I could expand this nicely however. As mentioned my other option was using recursion, which I am trying to make work now without much success. – lightxbulb Oct 24 '19 at 15:02
  • I think Igor's answer is the _nicest_ way. Depending on what you want to achieve exactly, you don't have to use recursion at all. – local-ninja Oct 24 '19 at 15:13
  • @fdan I am not exactly sure what the idea was about how to extract those `T` and `Is...`, and even less how to do this without recursion. What I managed to achieve you can find in my answer below. But as is clear, it just grows a vector, which is not very optimal. It is possible that a lot of the initial intent has been lost in the original post, since Barry simplified the example very much, to the point where I am uncertain whether it is clear what I was trying to achieve. However, this can be seen in the example in my answer. – lightxbulb Oct 24 '19 at 15:26

3 Answers3

1

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
 }
Community
  • 1
  • 1
max66
  • 65,235
  • 10
  • 71
  • 111
  • This seems to be doing exactly what I did in my tentative answer, but with growing tuples rather than vectors and without recursion (and obviously your method can be extended to produce a tuple from a scalar like what I did). Will that concatenated tuple, optimize to just a flat expansion in general (e.g. with -O2)? Since otherwise it will create N tuples in the worst case, requiring `N^2/2` elements total. – lightxbulb Oct 24 '19 at 15:34
  • 1
    @lightxbulb - Yes, the point of my answer is avoid the recursion. About optimization... frankly I don't know. – max66 Oct 24 '19 at 15:48
1

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

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • In my initial question (before it got edited by Barry), I had mentioned that the point was not about using `std::cout` and consequently `<<`. Since clearly then one can define the operator for the containers, and just call it as you did (this is what my initial example actually did, before it got edited out). The point is to pass a flat expansion to any function (not necessarily an operator that may be overloaded). And more specifically in this case, a constructor. The initial problem was trying to generate all possible constructors for a given vector, so if you have a vec4, you would have... – lightxbulb Oct 24 '19 at 15:54
  • ... ctors: `vec4(vec3, T)`, `vec4(T, vec3)`, `vec4(vec2,vec2)`, `vec4(vec2,T,T)`, `vec4(T,vec2,T)`, `vec4(T,T,vec2)`, `vec4(T,T,T,T)`. The point was to make the compiler generate those automatically, and doing so while producing the exact same code (in terms of performance) as if I were to write them by hand (if compiled with suitable optimization flags, e.g. -O2). One thing led to another, and I realized that C++ cannot pattern match as I had hoped it could (like Prolog for example). – lightxbulb Oct 24 '19 at 15:57
  • 1
    I understand the question as Barry... But in fact, you mostly want a [`std::apply`](https://en.cppreference.com/w/cpp/utility/apply) for multiple tuples. – Jarod42 Oct 24 '19 at 15:59
  • Yes I would say so - for any combination of tuples. Thought I am unsure whether I can call in a constructor from `std::apply`. By all means, max66's answer is probably the closest I can get to this, though I do not think that the compiler would optimize this, so most likely I would still have to write this by hand, or write a code generator. – lightxbulb Oct 24 '19 at 16:02
  • As far as what I mean, by the fact that C++ can't pattern match as well as I thought it could, see: https://coliru.stacked-crooked.com/a/1f41f3793846cdb1 , with `GROW_VECTOR 0`. Did I mess up something, or is it just impossible for the C++ compiler to actually match those constructors? – lightxbulb Oct 24 '19 at 16:15
  • @lightxbulb: Provided alternative with `multi_apply`. – Jarod42 Oct 24 '19 at 17:31
  • In `template void f(Ts1..., int, Ts2...)`, `Ts1...` is a non deduced context in that position. so results in empty pack for `f(a, b, c);`. (and whereas we can provide type in regular template function (f('*', 42)), we cannot with template constructor). – Jarod42 Oct 24 '19 at 17:39
0

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.

lightxbulb
  • 1,251
  • 12
  • 29