Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++14: How to group variadic inputs by template parameter?

Say I have two classes:

template <unsigned N>
class Pixel {
    float color[N];
public:
    Pixel(const std::initializer_list<float> &il)
    {
      // Assume this code can create a Pixel object from exactly N floats, and would throw a compiler error otherwise

    }
};

template <unsigned N>
class PixelContainer {
    std::vector<Pixel<N>> container;
};

What I'm trying to do is to write a constructor for PixelContainer such that: It would instantiate correctly for the following cases (example, not exhaustive):

PixelContainer<3> pc1(1, 2, 3)          // Creates a container containing one Pixel<3> objects
PixelContainer<3> pc2(1, 2, 3, 4, 5, 6) // Creates a container containing 2 Pixel<3> objects
PixelContainer<2> pc3(1, 2, 3, 4, 5, 6) // Creates a container containing 3 Pixel<2> objects

It would not compile for the following cases (as example, not exhaustive):

PixelContainer<3> pc4(2, 3) // Not enough arguments
PixelContainer<2> pc5(1, 2, 3, 4, 5) // Too many arguments

How do I achieve the above using template meta-programming? I feel it should be achievable, but can't figure out how. Specifically, I do not want to be doing the grouping myself e.g

PixelContainer<2> pc2({1, 2}, {3, 4}, {5, 6}) // Creates a container containing 3 Pixel<2> objects

(See this question for the inspiration behind mine)

like image 217
TCSGrad Avatar asked Jan 28 '23 04:01

TCSGrad


2 Answers

template<class T, std::size_t I, std::size_t...Offs, class Tup>
T create( std::index_sequence<Offs...>, Tup&& tup ) {
  return T( std::get<I+Offs>(std::forward<Tup>(tup))... );
}

template <unsigned N>
struct Pixel {
    float color[N];

    template<class...Ts,
        std::enable_if_t< sizeof...(Ts)==N, bool > = true
    >
    Pixel(Ts&&...ts):color{ std::forward<Ts>(ts)... } {};
};

template <unsigned N>
struct PixelContainer {

    std::vector<Pixel<N>> container;
    template<class T0, class...Ts,
      std::enable_if_t<!std::is_same<std::decay_t<T0>, PixelContainer>{}, bool> =true
    >
    PixelContainer(T0&& t0, Ts&&...ts):
      PixelContainer( std::make_index_sequence<(1+sizeof...(Ts))/N>{}, std::forward_as_tuple( std::forward<T0>(t0), std::forward<Ts>(ts)... ) )
    {}
    PixelContainer() = default;
private:
  template<class...Ts, std::size_t...Is>
  PixelContainer( std::index_sequence<Is...>, std::tuple<Ts&&...>&& ts ):
    container{ create<Pixel<N>, Is*N>( std::make_index_sequence<N>{}, std::move(ts) )... }
  {}
};

create takes a type, a starting index, and a index sequence of offsets. Then it takes a tuple.

It then creates the type from the (starting index)+(each of the offsets) and returns it.

We use this in the private ctor of PixelContainer. It has an index sequence element for each of the Pixels whose elements are in the tuple.

We multiply the index sequence element by N, the number of elements per index sequence, and pass that to create. Also, we pass in an index sequence of 0,...,N-1 for the offsets, and the master tuple.

We then unpack that into a {} enclosed ctor for container.

The public ctor just forwards to the private ctor with the right pack of indexes of one-per-element (equal to argument count/N). It has some SFINAE annoyance enable_if_t stuff to avoid it swallowing stuff that should go to a copy ctor.

Live example.

Also,

  std::enable_if_t<0 == ((sizeof...(Ts)+1)%N), bool> =true

could be a useful SFINAE addition to PixelContainer's public ctor.

Without it, we simply round down and discard "extra" elements passed to PixelContainer. With it, we get a "no ctor found" if we have extra elements (ie, not a multiple of N).

like image 143
Yakk - Adam Nevraumont Avatar answered Jan 30 '23 09:01

Yakk - Adam Nevraumont


Made something as well, which relies more on compiler optimizations for performance than @Yakk's answer.

It uses temporary std::arrays. temp is used to store the passed values somewhere. temp_pixels is used to copy pixel data from temp. Finally temp is copied into container.

I believe that those arrays do get optimized away, but I'm not certain. Looking at godbolt it seems that they are but I am not good at reading compiler assembly output :)

#include <array>
#include <algorithm>
#include <cstddef>
#include <vector>

template <unsigned N>
struct Pixel {
    float color[N]; // consider std::array here
};

template <unsigned N>
class PixelContainer {
    std::vector<Pixel<N>> container;

public:
    template<class... Ts>
    PixelContainer(Ts... values)
    {
        static_assert(sizeof...(Ts) % N == 0, "Pixels should be grouped by 3 values in PixelContainer constructor");
        const std::array<float, sizeof...(Ts)> temp{float(values)...};
        std::array<Pixel<N>, sizeof...(Ts) / N> temp_pixels{};

        for (std::size_t i = 0; i < sizeof...(Ts); i += N)
        {
            auto& pixel = temp_pixels[i / N];

            std::copy(
                temp.begin() + i, temp.begin() + i + N,
                pixel.color
            );
        }

        container = std::vector<Pixel<N>>(temp_pixels.begin(), temp_pixels.end());
    }
};

int main()
{
    PixelContainer<3> pc1(1, 2, 3);          // Creates a container containing one Pixel<3> objects
    PixelContainer<3> pc2(1, 2, 3, 4, 5, 6); // Creates a container containing 2 Pixel<3> objects
    PixelContainer<2> pc3(1, 2, 3, 4, 5, 6); // Creates a container containing 3 Pixel<2> objects

/*
    PixelContainer<3> pc4(2, 3); // Not enough arguments
    PixelContainer<2> pc5(1, 2, 3, 4, 5); // Too many arguments
*/
}
like image 32
Asu Avatar answered Jan 30 '23 09:01

Asu