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)
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).
Made something as well, which relies more on compiler optimizations for performance than @Yakk's answer.
It uses temporary std::array
s. 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
*/
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With