Fabian Knorr
Fabian Knorr

Reputation: 3184

Class with nested brace-enclosed initializers

I want to create a recursive, multi-dimensional array class using variadic templates.

#include <array>

template<int...> struct multi;

template<int Body>
struct multi<Body>: std::array<int, Body> {        
    template<typename... Params>
    multi(int first, const Params&... args)
        : std::array<int, Body> {{ first, args... }} {
    }
};

template<int Head, int Body, int... Tail>
struct multi<Head, Body, Tail...>: std::array<multi<Body, Tail...>, Head> {
    template<typename... Params>
    multi(const multi<Body, Tail...>& first, const Params&... args)
        : std::array<multi<Body, Tail...>, Head> {{ first, args... }} {
    }
};

To initialize an instance of this, class, I have to do the following:

multi<2, 2> m { multi<2>{ 1, 2 }, multi<2>{ 3, 4 } };

I'd really appreciate if I could just use a C-array-like style for uniform initialization like so:

multi<2, 2> m { { 1, 2 }, { 3, 4 } };

Unfortuanely, this doesn't work as it seems to be impossible to handle these nested initialiers:

multi.cc: In function 'int main()':
multi.cc:25:40: error: no matching function for call to 'multi<2, 2>::multi(<brace-enclosed initializer list>)'
     multi<2, 2> m { { 1, 2 }, { 3, 4 } };
                                        ^
multi.cc:25:40: note: candidates are:
multi.cc:19:5: note: multi<Head, Body, Tail ...>::multi(const multi<Body, Tail ...>&, const Params& ...) [with Params = {}; int Head = 2; int Body = 2; int ...Tail = {}]
     multi(const multi<Body, Tail...>& first, const Params&... args)
     ^
multi.cc:19:5: note:   candidate expects 1 argument, 2 provided
multi.cc:17:8: note: constexpr multi<2, 2>::multi(const multi<2, 2>&)
 struct multi<Head, Body, Tail...>: std::array<multi<Body, Tail...>, Head> {
        ^
multi.cc:17:8: note:   candidate expects 1 argument, 2 provided
multi.cc:17:8: note: constexpr multi<2, 2>::multi(multi<2, 2>&&)
multi.cc:17:8: note:   candidate expects 1 argument, 2 provided

Is there any way to achieve this?

Upvotes: 2

Views: 1437

Answers (2)

Brian Bi
Brian Bi

Reputation: 119239

You are trying to initialize your class like an aggregate, but it isn't an aggregate because it has a user-provided constructor.

You can remove the user-defined constructors and make your class an aggregate, as hvd explains. Or, you can make your constructor take std::initializer_list. However, this approach has some caveats...

#include <array>
#include <initializer_list>
#include <algorithm>

template<int...> struct multi;

template<int Body>
struct multi<Body>: std::array<int, Body> {
    multi() = default;
    multi(std::initializer_list<int> l) {
        std::copy(l.begin(), l.end(), std::array<int, Body>::begin());
    }
};

template<int Head, int Body, int... Tail>
struct multi<Head, Body, Tail...>: std::array<multi<Body, Tail...>, Head> {
    multi() = default;
    multi(std::initializer_list<multi<Body, Tail...>> l) {
        for (int i = 0; i < Head; i++) {
            (*this)[i] = multi<Body, Tail...>(l.begin()[i]);
        }
    }
};

int main() {
    multi<2, 2> m {{1, 2}, {3, 4}}; // OK
    multi<2, 1000> m2 {{1, 2}, {3, 4}}; // constructs temporary arrays of size 1000 and copies them---expensive!
    multi<2> m3 {1, 2, 3}; // too many initializers, but no compile error!
}

The issue with unnecessary copying can be resolved by passing the initializer list in its original form, a (possibly) nested initializer list of ints, and iterating at each level. But in this case template parameter deduction doesn't work and you need a helper class. Also, you still can't trigger a compile error when too many initializers are given.

#include <array>
#include <initializer_list>
#include <algorithm>

// IL<n>::type is an n-times nested initializer list of ints
template<int n> struct IL {
    typedef std::initializer_list<typename IL<n-1>::type> type;
};

template<> struct IL<1> {
    typedef std::initializer_list<int> type;
};

template<int...> struct multi;

template<int Body>
struct multi<Body>: std::array<int, Body> {
    multi() = default;
    multi(const std::initializer_list<int>& l) {
        assign(l);
    }
    void assign(const std::initializer_list<int>& l) {
        std::copy(l.begin(), l.end(), std::array<int, Body>::begin());
    }
};

template<int Head, int Body, int... Tail>
struct multi<Head, Body, Tail...>: std::array<multi<Body, Tail...>, Head> {
    multi() = default;
    multi(const typename IL<2 + sizeof... Tail>::type& l) {
        assign(l);
    }
    void assign(const typename IL<2 + sizeof... Tail>::type& l) {
        for (int i = 0; i < l.size(); i++) {
            (*this)[i].assign(l.begin()[i]);
        }
    }
};

Upvotes: 0

user743382
user743382

Reputation:

You can get pretty close, if you don't mind multiple braces:

template<int...> struct multi;

template <int N>
struct multi<N> {
  int elems[N];
};

template <int Head, int... Tail>
struct multi<Head, Tail...> {
  multi<Tail...> elems[Head];
};

int main() {
  multi<2, 2, 2> m {{ {{ {{ 1, 2 }}, {{ 3, 4 }} }}, {{ {{ 5, 6 }}, {{ 7, 8 }} }} }};
}

What I've done is removed the inheritance and the custom constructor, which makes it possible to get this working with the compiler's own support for aggregate initialisation.

The fact that multiple braces are needed is unfortunate, but I don't see an easy way of avoiding that, and it doesn't hurt that much for readability, in my opinion.

A slightly more complicated solution, but still not very complicated, is to not nest multi<...>, but to use a multidimensional array:

template<int...> struct helper;

template<int N>
struct helper<N> { typedef int type[N]; };

template<int Head, int... Tail>
struct helper<Head, Tail...> { typedef typename helper<Tail...>::type type[Head]; };

template<int... Ns>
struct multi {
  typename helper<Ns...>::type elems;
};

int main() {
  multi<2> m1 { 1, 2 };
  multi<2, 2> m2 {{ { 1, 2 }, { 3, 4 } }};
  multi<2, 2, 2> m3 {{ { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }};
}

You do still need multiple braces, but only at the outer level.

If it turns out you actually don't need a class at all, though, it can be done:

template<int...> struct helper;

template<int N>
struct helper<N> { typedef int type[N]; };

template<int Head, int... Tail>
struct helper<Head, Tail...> { typedef typename helper<Tail...>::type type[Head]; };

template<int... Ns>
using multi = typename helper<Ns...>::type;

int main() {
  multi<2> m1 { 1, 2 };
  multi<2, 2> m2 { { 1, 2 }, { 3, 4 } };
  multi<2, 2, 2> m3 { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } };
}

But as multi<2, 2, 2> is now merely int[2][2][2], you cannot add any methods (or anything similar) any more.

Upvotes: 1

Related Questions