简体   繁体   中英

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 eg

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
*/
}

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;
};

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM