Is it possible to mimic the template argument deduction of nested initializer_lists with variadic templates?

Viewed 58

I have an N-dimensional vector class ndvector with the following constructors (definitions excluded):

template <typename T> class ndvector {
public:
  constexpr ndvector(std::initializer_list<T> il);

  constexpr ndvector(std::initializer_list<ndvector<T>> il);
};

These constructors allow for the initialization of ndvectors of any dimension:

ndvector a{1, 2};

ndvector b{a, {3, 4}};
ndvector<int> c{{1, 2}, {3, 4}};

ndvector d{b, {a, {3, 4}}};
ndvector<int> e{{{1, 2}, {3, 4}}, {{1, 2}, {3, 4}}};

For a reason I don't fully understand, T must be explicitly specified in some cases, but that is a small price to pay.

That being said, I ran into some issues while attempting to achieve similar functionality with a statically-allocated ndarray class:

template <typename T, std::size_t... DIMS> class ndarray;

template <typename T, std::size_t DIM, std::size_t... DIMS>
class ndarray<T, DIM, DIMS...> {
public:
  template <typename... Ts>
  constexpr ndarray(Ts &&...elements);

  template <std::convertible_to<ndarray<T, DIMS...>>... Ts>
  constexpr ndarray(Ts &&...elements);
};

template <typename T, std::same_as<T>... Ts>
ndarray(T, Ts...) -> ndarray<T, 1 + sizeof...(Ts)>;

template <typename T, std::same_as<T>... Ts, std::size_t... DIMS>
ndarray(ndarray<T, DIMS...>, ndarray<Ts, DIMS...>...)
    -> ndarray<T, 1 + sizeof...(Ts), DIMS...>;

Even with explicit template arguments, the last line in the snippet below fails to compile:

ndarray a{1, 2};
ndarray<int, 2> b{3, 4};

ndarray c{a, b};
ndarray<int, 2, 2> d{{1, 2}, {3, 4}};

(Note: at the time of writing, MSVC will encounter an internal compiler error for any instantiation of ndarray due to a bug. I am using GCC 11, but GCC 10 and various versions of Clang also work).

As far as I am aware, this is because {1, 2}, etc. are treated as initializer lists that cannot be resolved to a type. Unfortunately, I cannot use std::initializer_list because they rarely form constant expressions, which would mean that DIMS could never be determined implicitly. I have also tried the following two constructors, to no avail:

template <typename... Ts> requires (... || !std::convertible_to<Ts, ndarray<T, DIMS...>>)
constexpr ndarray(Ts &&...elements);

template<typename... Ts>
constexpr ndarray(const ndarray<Ts, DIMS...> &...elements) {}
constexpr ndarray(const ndarray<T, DIMS...> elements...) {}

The first alternative fails for the same reason - Ts cannot be deduced from the untyped initializer list. C-style variadic arguments don't work either, as the first initializer list will seemingly attempt to initialize elements. Is there another approach that could bear more fruit?

1 Answers

Whereas I don't think there is a solution for CTAD and {..}, you might turn your class to have exactly expected number of parameters instead of using forwarding reference, something like:

// Helper to have fixed type when folding std::index_sequence<Is...>
template <std::size_t, typename T> using always_t = T;

template <typename T, typename... Seqs>
struct ndarray_impl;

template <typename T>
struct ndarray_impl<T>
{
    ndarray_impl(const T& data) : data(data) {}

    T data;
};

template <typename T, std::size_t... Is, typename... Seqs>
struct ndarray_impl<T, std::index_sequence<Is...>, Seqs...>
{
    ndarray_impl(const always_t<Is, ndarray_impl<T, Seqs...>>&... args) : data{args...} {}

    std::array<ndarray_impl<T, Seqs...>, sizeof... (Is)> data;
};

template <typename T, std::size_t... Is>
using ndarray = ndarray_impl<T, std::make_index_sequence<Is>...>;

Demo

Related