3

I want to be able to use parameter pack expansion in the initializer list of constructors. Is the best way to achieve this, to endow my class with a parameter pack template argument? Here is an example of what I mean: https://coliru.stacked-crooked.com/a/e699c4cd035e0b1c

#include <utility>
#include <iostream>

template<typename T, std::size_t...Is>
struct base_vec
{
    constexpr static std::size_t N = sizeof...(Is);
    T e[N];
    base_vec() : e{} {}
    explicit base_vec(const T& s) : e{((void)Is,s)...} {}
};


template<typename T, std::size_t...Is>
std::ostream& operator<<(std::ostream& lhs, const base_vec<T,Is...>& rhs)
{
    return (std::cout << ... << rhs.e[Is]);
}



template<typename T, std::size_t...Is>
constexpr auto getVecIs(std::index_sequence<Is...> seq)
{
    return base_vec<T, Is...>{};
}




template<typename T, std::size_t N>
using vec = decltype(getVecIs<T>(std::declval<std::make_index_sequence<N>>()));

int main()
{
    vec<int,3> v(2);
    std::cout << v << "\n";
    return 0;
}

Note the trivial expansion e{((void)Is,s)...}. Is this an ok practice, or am I missing some minus of this approach (except for the fact that now my structure will have a whole parameter pack, as opposed to a single size_t N)?

max66
  • 65,235
  • 10
  • 71
  • 111
lightxbulb
  • 1,251
  • 12
  • 29
  • It entirely depends on what you want `vec` to represent, what you want its API to look like, and how you want it to be used. What you have done seems reasonable... assuming that you want something like `std::array`. – Vittorio Romeo Oct 22 '19 at 12:06
  • @lightxbulb You can always use `std::fill` in combination with `std::begin` and `std::end` on the member array in the constructor of the class. – Petok Lorand Oct 22 '19 at 12:14
  • @PetokLorand `std::fill` is not compile time. – lightxbulb Oct 22 '19 at 12:15
  • And that assumes that `T` is default constructible. – Jarod42 Oct 22 '19 at 12:17
  • @lightxbulb Your member array is not compile-time either, it's a normal array that will be allocated on the stack, and populated at run-time in your example too. – Petok Lorand Oct 22 '19 at 12:17
  • @PetokLorand If you add in some `constexpr` here and there, the operations are compile time - meaning that you will get a `e{s,s,...,s}` in code, whereas with a for loop this is not the case - produces also different assembly. – lightxbulb Oct 22 '19 at 12:18
  • @lightxbulb Take a look at godbolt, and the generated code for the constructor https://godbolt.org/z/lI44bF. Note that the optimizations are disabled to not let the compiler optimize away the call, the compiler optimization would hide the fact that it's not required to be done at compile time. – Petok Lorand Oct 22 '19 at 12:19
  • 1
    @PetokLorand: Add missing `constexpr`, and OP code would be `constexpr` too [Demo](https://godbolt.org/z/xzgxTr), but [`std::fill`](https://en.cppreference.com/w/cpp/algorithm/fill) would be `constexpr` in C++20. so anyway, it was a wrong argument. – Jarod42 Oct 22 '19 at 12:20

2 Answers2

3

You can move the expansion entirely inside the class:

template<typename T, std::size_t N>
struct vec
{
public:
    T e[N];
    vec() : e{} {}
    explicit vec(const T& s) : vec{s, std::make_index_sequence<N>{}} {}
private:
    template <std::size_t ... Is>
    vec(const T& s, std::index_sequence<Is...>) : e{(Is, s)...} {}
};
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • I actually have written the exact same version previously! I wasn't sure which is better though. The version I have written here, allows to do this without making extra functions, and that number increases with every other function (for example for `operator<<`. The only thing I am not sure about is, if I have `vec`, then I will need to expand the two parameter packs, but then C++ cannot differentiate those in the context: `base_vec`. – lightxbulb Oct 22 '19 at 12:12
  • @lightxbulb it can, if they are deduced. If you often need to iterate over your array at compile-time, you can provide a higher-order function to do that as well. – Quentin Oct 22 '19 at 12:14
  • @lightxbulb: For `operator<<`, you might use regular for range. For generic case, you might still have the possibility to create a template member `for_each`, to avoid to duplicate each method. – Jarod42 Oct 22 '19 at 12:16
  • @Quentin Could you elaborate, since I am getting an error: `error: parameter pack 'Is' must be at the end of the template parameter list`. I am not exactly sure how to combine two non-type parameter packs without ambiguity. Maybe with dummy templates? – lightxbulb Oct 22 '19 at 12:16
  • One of the main points in OPs example seems to be to have the parameter pack readily available as used in `operator<<`. That makes this seem like a step backwards. – super Oct 22 '19 at 12:37
  • @super: Not sure what was the main point of OP, I provide solution without intermediate class. BTW, OP's code already works... – Jarod42 Oct 22 '19 at 12:44
  • 1
    @lightxbulb hard to say without seeing your code, but [this](https://wandbox.org/permlink/YyF7SUd2suxJSysU) is what I mean. It does require some hoop-hopping. – Quentin Oct 22 '19 at 12:54
  • @Quentin I see it after the examples presented, it seems like I hadn't provided the arguments required for deduction in my case, which was causing the issue. – lightxbulb Oct 22 '19 at 12:58
2

It seems to me that you're recreating a class inspired to std::array with an handy index sequence to operate.

Intriguing.

I propose a simplification (IMHO) that directly define vec, avoiding base_vec, using template specialization

#include <utility>
#include <iostream>

template <typename T, std::size_t N, typename = std::make_index_sequence<N>>
struct vec;

template <typename T, std::size_t N, std::size_t...Is>
struct vec<T, N, std::index_sequence<Is...>>
 {
    T e[N] {} ;

    vec (const T& s) : e{((void)Is,s)...}
     { }
 };


template <typename T, std::size_t...Is>
std::ostream& operator<< (
   std::ostream& lhs,
   vec<T, sizeof...(Is), std::index_sequence<Is...>> const & rhs)
 { return (std::cout << ... << rhs.e[Is]); }

int main ()
 {
    vec<int, 3u> v(2);

    std::cout << v << "\n";
 }

You can add, in the body of the vec specialization, a static_assert() as follows

static_assert( N == sizeof...(Is) );

to avoid "hijacked" uses as follows

vec<int, 5u, std::make_index_sequence<12u>>  bad_vec_1;

or, maybe better,

static_assert( std::is_same_v<std::index_sequence<Is...>,
                              std::make_index_sequence<N>> );   

to avoid aslo

vec<int, 5u, std::index_sequence<1u, 3u, 5u, 2u, 4u>>  bad_vec_2;

-- EDIT --

The OP asks

Any ideas how to extend this to multiple index sequences - for multi-dimensional arrays for example?

It's simple.

As suggested by Jarod42, you can add another N and anther std::index_sequence.

I propose as follows for the 2 dim case.

I've also added a third index sequence for N1*N2 (I suppose can be useful).

I leave the body of the specialization as exercise.

template <typename T, std::size_t N1, std::size_t N2,
          typename = std::make_index_sequence<N1>,
          typename = std::make_index_sequence<N2>,
          typename = std::make_index_sequence<N1*N2>>
struct vec2dim;

template <typename T, std::size_t N1, std::size_t N2,
          std::size_t ... Is, std::size_t ... Js, std::size_t ... Ks>
struct vec2dim<T, N1, N2, std::index_sequence<Is...>,
               std::index_sequence<Js...>, std::index_sequence<Ks...>>
 {
   static_assert( std::is_same_v<std::index_sequence<Is...>,
                                 std::make_index_sequence<N1>> );
   static_assert( std::is_same_v<std::index_sequence<Js...>,
                                 std::make_index_sequence<N2>> );
   static_assert( std::is_same_v<std::index_sequence<Ks...>,
                                 std::make_index_sequence<N1*N2>> );
   // ...
 };
max66
  • 65,235
  • 10
  • 71
  • 111
  • But `vec>` would be partially usable – Jarod42 Oct 22 '19 at 12:26
  • I was initially trying to make the version you have above, but didn't quite manage to get the code right. Any ideas how to extend this to multiple index sequences - for multi-dimensional arrays for example? How will the compiler differentiate between the two parameter packs? Would some dummy templates help? Or should I go for a nested approach? – lightxbulb Oct 22 '19 at 12:26
  • @Jarod42 - answer improved to solve this type of problems (see second proposed `static_assert()`) – max66 Oct 22 '19 at 12:28
  • @lightxbulb: Similarly, you might do: `template , typename = std::make_index_sequence> struct vec2;`. – Jarod42 Oct 22 '19 at 12:29
  • @Jarod42 How would the `template ` part work out though? I get the following error if I try to use two parameter packs: `error: parameter pack 'Ns' must be at the end of the template parameter list` since it's obviously ambiguous. I can make a new question if this is deemed too unrelated to the current one. – lightxbulb Oct 22 '19 at 12:31
  • 1
    @lightxbulb: [Demo](http://coliru.stacked-crooked.com/a/0cfab5d939d62fe8) – Jarod42 Oct 22 '19 at 12:35
  • 1
    @lightxbulb - answer improved for 2D case (but, I see now, is almost the same in the Jarod42 demo) – max66 Oct 22 '19 at 12:39