4

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)

TCSGrad
  • 11,898
  • 14
  • 49
  • 70

4 Answers4

4
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).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    I think OP would appreciate some enlightenment to why and how it works. – Hatted Rooster Nov 08 '18 at 19:57
  • The name of the offset pack for `create` is inconsistent, should be `Is`. – Pezo Nov 08 '18 at 20:00
  • 4
    @SombreroChicken If one seeks enlightenment, one should seek to become budda. If one seeks confusion, one should learn C++. – Yakk - Adam Nevraumont Nov 08 '18 at 20:00
  • @Pezo Fixed! Made it `Offs` because they are offsets. – Yakk - Adam Nevraumont Nov 08 '18 at 20:00
  • @Yakk-AdamNevraumont - The answer taught me so many `std::` constructs in one post! Can you elaborate on the last edit, about the SFINAE addition to the public actor (and where it might be useful) ? Thanks for refining the answer and the live example! – TCSGrad Nov 08 '18 at 20:14
  • @TCSGrad It means that you'll get "no constructor found" if you don't pass a multiple-of-N to the PixelContainer ctor, instead of silently dropping elements or generating an error deep within template expansions. – Yakk - Adam Nevraumont Nov 08 '18 at 20:22
3

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
*/
}
asu
  • 1,875
  • 17
  • 27
1

I would propose some hybrid version, there is still a temporary array, but no temporary for pixels

template <unsigned N>
struct PixelContainer {

    template<std::size_t S, std::size_t... elts>
    auto createOne(const std::array<float, S> &arr, std::size_t offset,
                   std::index_sequence<elts...>) {
        return Pixel<N>{ arr[offset + elts]... };
    }

    template<typename... Floats>
    PixelContainer(Floats... vals) {
        static_assert(sizeof...(vals) % N == 0, "invalid number of args");
        std::array<float, sizeof...(vals)> arr = { float(vals)... };
        for (size_t i = 0; i < sizeof...(vals) / N; i++) {
            container.push_back(createOne(arr, i * N, std::make_index_sequence<N>{}));
        }

    }
    std::vector<Pixel<N>> container;
};
OznOg
  • 4,440
  • 2
  • 26
  • 35
0

I think the answer is pretty much given in the link you provided (C++ number of function's parameters fixed by template parameter). You just need to change the assert there: instead of sizeof...(Floats) == N you'll want sizeof...(Floats) % N == 0.

Henning Koehler
  • 2,456
  • 1
  • 16
  • 20
  • That doesn't show me how to group the inputs so that I can instantiate the Pixel objects one by one. Let me try to edit the question to reflect that. – TCSGrad Nov 08 '18 at 19:52