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?
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.
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]);
}
}
};
Related
I am trying to use variadic template and constructor arguments to initialise values of a multi dimensional arrays inside a custom class ArrayND. Up until this point I've been successful initialising arbitrary dimensional Array instances with multi dimensional std::arrays, but the double bracing that's mostly but not always needed for std::array initialisation gets a bit ugly and hard to parse, for example:
constinit ArrayND<5, 4, 6> my_nd_array({
{
{{ {0.4f}, {0.6f}, {0.1f}, {0.4f} }},
{0.4f, 0.6f, 0.1f, 0.4f},
{0.4f, 0.6f, 0.1f, 0.4f},
{0.4f, 0.6f, 0.1f, 0.4f},
{0.4f, 0.6f, 0.1f, 0.4f},
}
});
So far, I can initialise a 1D implementation of the Array class without trouble, although I'd rather require a single set of curly braces around the values in this instance:
template<size_t SIZE>
struct Array1D {
template<class ... VALUES>
constexpr explicit Array1D(const VALUES ... values)
: data({values...})
{}
std::array<float, SIZE> data;
};
constinit Array1D<2> my_2_array(
0.1f, 0.2f
);
But I'm not sure if/how it's possible to do a similar thing to cleanly initialise higher dimensional arrays. I've been experimenting with the Array2D, but neither version of the initialisation I've pasted below works:
template<size_t SIZE_0, size_t SIZE_1>
struct Array2D {
template<class ... VALUES>
constexpr explicit Array2D(const VALUES ... values)
: data(values...)
{}
std::array<std::array<float, SIZE_1>, SIZE_0> data;
};
// ERROR: No matching constructor...
constinit Array2D<2, 2> my_2x2_array_A(
{ 0.1f, 0.2f }, { 0.3f, 0.4f }
);
// ERROR: No matching constructor...
constinit Array2D<2, 2> my_2x2_array_B(
{ { 0.1f, 0.2f }, { 0.3f, 0.4f } }
);
However, the eventual goal is to be able to do the same thing with my ArrayND currently which looks like this:
template<size_t SIZE, size_t ... SUB_SHAPE>
struct ArrayND {
template<class ... VALUES>
constexpr explicit ArrayND(const VALUES ... values)
: data(values...)
{}
using DataType = std::array<typename ArrayND<SUB_SHAPE...>::DataType, SIZE>;
DataType data;
};
Anyone got any ideas on how to achieve something like this without passing in a multi dimensional std::array?
To clarify, the constexpr constructor is important to what I need to do. I have a std::vector version that works just fine, but std::vector still has no consexpr constructor in clang 13.0 (and I'm not sure it's even accepted into the standard anyway, it's possible only MSVC implemented this feature...).
EDIT:
It's possibly worth noting that in the non-cutdown implementations, these arrays will all derive from the final ND class, with specialisations for scalar and 1D versions. The Array1D and Array2D classes are just for testing solutions to this issue in a simpler form.
UPDATE:
I'd rather not end up managing and writing algorithms for C arrays if I can avoid it, but it occurred to me I hadn't tried at least using them for construction... But I I'm struggling to get that working as well:
template<size_t SIZE_0, size_t SIZE_1>
struct Array2D {
constexpr explicit Array2D(const float values[SIZE_0][SIZE_1])
: data(values)
{}
float data[SIZE_0][SIZE_1];
};
// ERROR: No matching constructor...
constinit Array2D<2, 2> my_2x2_array_0(
{{ 0.1f, 0.2f }, { 0.3f, 0.4f }}
);
// ERROR: No viable conversion from 'float[2][2]' to 'Array2D<2, 2>'. Explicit constructor is not a candidate
// Personal note: It appears to be trying to use the copy constructor, which I guess kind of makes sense
constinit float raw_data[2][2] = {{ 0.1f, 0.2f }, { 0.3f, 0.4f }};
constinit Array2D<2, 2> my_2x2_array_1(raw_data);
1. Using aggregate initialization (easy)
If you just leave out your constructor and make the data field public you can take advantage of aggregate initialization:
template<std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
struct Array3D {
std::array<std::array<std::array<float, SIZE_2>, SIZE_0>, SIZE_0> data;
};
constinit Array3D<2,2,2> arr{
{{
{{
{1, 2},
{3, 4},
}},
{{
{5, 6},
{7, 8},
}}
}}
};
(godbolt example)
This also allows type-conversion (using ints in the example), however it'll take quite a few extra curly-braces.
2. passing in a multidimensional std::array (easy)
template<std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
struct Array3D {
constexpr Array3D(std::array<std::array<std::array<float, SIZE_2>, SIZE_0>, SIZE_0> arr)
: data(arr) {}
std::array<std::array<std::array<float, SIZE_2>, SIZE_0>, SIZE_0> data;
};
constinit Array3D<2,2,2> arr{
{{
{{
{1, 2},
{3, 4},
}},
{{
{5, 6},
{7, 8},
}}
}}
};
(godbolt example)
This basically behaves the same way as 1., with the additional benefit that you can make data private.
3. Use a c array instead of std::array (easy)
By using a c array you can use aggregate initialization like you want without double braces:
template<size_t SIZE_0, size_t SIZE_1, size_t SIZE_2>
struct Array3D {
float data[SIZE_0][SIZE_1][SIZE_2];
};
constinit Array3D<2,2,2> arr {
{
{
{1, 2},
{3, 4}
},
{
{5, 6},
{7, 8}
}
}
};
(godbolt example)
4. Pass a c array as constructor parameter
By passing a c array you get the same effect as in 3., with the added benefit of being able to keep the std::array's
template<std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
struct Array3D {
using array_type = float[SIZE_0][SIZE_1][SIZE_2];
constexpr Array3D(array_type const& values) {
for(int i = 0; i < SIZE_0; i++) {
for(int j = 0; j < SIZE_1; j++) {
for(int k = 0; k < SIZE_2; k++) {
data[i][j][k] = values[i][j][k];
}
}
}
}
std::array<std::array<std::array<float, SIZE_0>, SIZE_1>, SIZE_2> data;
};
constinit Array3D<2,2,2> arr {
{
{
{1, 2},
{3, 4}
},
{
{5, 6},
{7, 8}
}
}
};
(godbolt example)
5. Using std::initializer_list (hard)
With std::initializer_list you get the kind of initialization you want.
However you'll have to do quite a lot of work that the compiler would do for you in 1. / 2., e.g. verifying the sizes of the lists and filling missing numbers with 0.
// Helper to get the array type for an n-dimensional array
// e.g.: n_dimensional_array_t<float, 2, 2> == std::array<std::array<float, 2>, 2>
template<class T, std::size_t... dimensions>
struct n_dimensional_array;
template<class T, std::size_t... dimensions>
using n_dimensional_array_t = typename n_dimensional_array<T, dimensions...>::type;
template<class T, std::size_t last>
struct n_dimensional_array<T, last> {
using type = std::array<T, last>;
};
template<class T, std::size_t first, std::size_t... others>
struct n_dimensional_array<T, first, others...> {
using type = std::array<n_dimensional_array_t<T, others...>, first>;
};
// Helper to construct nested initializer_lists
// e.g.: nested_initializer_list_t<int, 2> == std::initializer_list<std::initializer_list<int>>
template<class T, std::size_t levels>
struct nested_initializer_list {
using type = std::initializer_list<typename nested_initializer_list<T, levels - 1>::type>;
};
template<class T>
struct nested_initializer_list<T, 0> {
using type = T;
};
template<class T, std::size_t levels>
using nested_initializer_list_t = typename nested_initializer_list<T, levels>::type;
// Helper to recursively fill a n-dimensional std::array
// with a nested initializer list.
template<class T, std::size_t... dimensions>
struct NestedInitializerListHelper;
template<class T, std::size_t last>
struct NestedInitializerListHelper<T, last> {
static constexpr void initialize(
std::array<T, last>& arr,
std::initializer_list<T> init
) {
if(init.size() > last)
throw std::invalid_argument("Too many initializers for array!");
for(int i = 0; i < last; i++) {
if(i < init.size())
arr[i] = std::data(init)[i];
else
arr[i] = {};
}
}
};
template<class T, std::size_t first, std::size_t... other>
struct NestedInitializerListHelper<T, first, other...> {
static constexpr void initialize(
n_dimensional_array_t<T, first, other...>& arr,
nested_initializer_list_t<T, 1 + sizeof...(other)> init
) {
if(init.size() > first)
throw std::invalid_argument("Too many initializers for array!");
for(int i = 0; i < first; i++) {
if(i < init.size())
NestedInitializerListHelper<T, other...>::initialize(
arr[i],
std::data(init)[i]
);
else
NestedInitializerListHelper<T, other...>::initialize(
arr[i],
{}
);
}
}
};
template<class T, std::size_t... dimensions>
struct MultidimensionalArray {
using array_type = n_dimensional_array_t<T, dimensions...>;
using initializer_type = nested_initializer_list_t<T, sizeof...(dimensions)>;
// Initializer-list constructor for nice brace-initialization
constexpr explicit MultidimensionalArray(initializer_type init) {
using init_helper = NestedInitializerListHelper<T, dimensions...>;
init_helper::initialize(data, init);
}
// (optional)
// array-based constructor to allow slicing of Multidimensional arrays,
// and construction from a list of numbers
// e.g.:
// // slicing
// MultidimensionalArray<int, 2, 2> k;
// MultidimensionalArray<int, 2> another(k[0]);
//
// // initialization from flat array / list of values
// MultidimensionalArray<int, 2, 2> j {{1,2,3,4}};
constexpr explicit MultidimensionalArray(array_type arr): data(arr) {
}
// (optional)
// array indexing operator
constexpr auto& operator[](std::size_t i) {
return data[i];
}
// (optional)
// array indexing operator
constexpr auto const& operator[](std::size_t i) const {
return data[i];
}
// (optional)
// allow conversion to array type (for slicing)
explicit constexpr operator array_type() {
return data;
}
private:
array_type data;
};
constinit MultidimensionalArray<float, 2, 2, 2> arr{
{
{
{1, 2},
{3, 4}
},
{
{5, 6},
{7, 8}
}
}
};
I've also combined the 1D / 2D / nD cases into a single class for less boilerplate.
This behaves just as one would expect it, you can leave out any part and that one would be initialized with 0, e.g.:
constinit MultidimensionalArray<float, 2, 2, 2> arr{
{
{},
{
{1, 2}
}
}
};
// inits arr with:
// 0 0
// 0 0
// 1 2
// 0 0
There are also a few optional methods, that add a few convenience features like slicing support and indexing operators.
If you want you can test in on godbolt.
std::array indeed requires double brace as it uses aggregate initialization.
You might create your version which has a constructor taking N parameters instead:
// helper used for variadic expension
template <std::size_t, typename T> using always_t = T;
template <typename T, typename Seq> struct my_array_impl;
template <typename T, std::size_t... Is>
struct my_array_impl<T, std::index_sequence<Is...>>
{
// Here, constructor is not template <typename ... Ts>
// {..} has no type, and can only be deduced
// to some types we are not interested in
// We use always_t<Is, T> trick to have constructor similar to
// my_array_impl(T arg1, T arg2, .., T argN)
constexpr my_array_impl(always_t<Is, T>... args) : data{{args...}} {}
// ...
std::array<T, sizeof...(Is)> data;
};
template <typename T, std::size_t N>
using my_array = my_array_impl<T, std::make_index_sequence<N>>;
and then
// just compute type alias currently
// to have my_array<my_array<..>, Size>
template <typename T, std::size_t I, std::size_t ... Is>
struct ArrayND_impl<T, I, Is...>
{
using DataType = my_array<typename ArrayND_impl<T, Is...>::DataType, I>;
};
template <typename T, std::size_t ... Sizes>
using ArrayND = typename ArrayND_impl<T, Sizes...>::DataType;
With usage similar to:
template <std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
using Array3D = ArrayND<float, SIZE_0, SIZE_1, SIZE_2>;
constinit Array3D<2,2,2> arr(
{
{1, 2},
{3, 4},
},
{
{5, 6},
{7, 8},
}
);
Demo.
Thanks #Turtlefight and #Jarod42 for your answers. I haven't had as much time to look at yours yet #Jarod42, but I shall. I've been working at my solution since #Turtlefight posted, and I thought it'd be silly not to share my current working solution having taken onboard the suggestions.
Godbolt link here!
Here's the code defining the classes and the helper functions:
template<class VALUE_TYPE, uint32_t SIZE, uint32_t ... SUB_SHAPE>
struct ArrayND;
template<class VALUE_TYPE, uint32_t SIZE>
constexpr auto init(const typename ArrayND<VALUE_TYPE, SIZE>::InitType& initializer_list) -> typename ArrayND<VALUE_TYPE, SIZE>::DataType
{
typename ArrayND<VALUE_TYPE, SIZE>::DataType array;
for (int i = 0; i < initializer_list.size(); i++) {
array[i] = std::data(initializer_list)[i];
}
return array;
}
template<class VALUE_TYPE, uint32_t SIZE, uint32_t SUB_SIZE, uint32_t ... SUB_SUB_SHAPE>
constexpr auto init(const typename ArrayND<VALUE_TYPE, SIZE, SUB_SIZE, SUB_SUB_SHAPE...>::InitType& initializer_list) -> typename ArrayND<VALUE_TYPE, SIZE, SUB_SIZE, SUB_SUB_SHAPE...>::DataType
{
typename ArrayND<VALUE_TYPE, SIZE, SUB_SIZE, SUB_SUB_SHAPE...>::DataType array;
for (int i = 0; i < initializer_list.size(); i++) {
array[i] = init<VALUE_TYPE, SUB_SIZE, SUB_SUB_SHAPE...>(std::data(initializer_list)[i]);
}
return array;
}
template<class VALUE_TYPE, uint32_t SIZE, uint32_t ... SUB_SHAPE>
struct ArrayND
{
using InitType = std::initializer_list<typename ArrayND<VALUE_TYPE, SUB_SHAPE...>::InitType>;
using DataType = std::array<typename ArrayND<VALUE_TYPE, SUB_SHAPE...>::DataType, SIZE>;
constexpr explicit ArrayND(const InitType& initializer_list)
: data(init<VALUE_TYPE, SIZE, SUB_SHAPE...>(initializer_list))
{}
DataType data;
};
template<class VALUE_TYPE, uint32_t SIZE>
struct ArrayND<VALUE_TYPE, SIZE>
{
using InitType = std::initializer_list<VALUE_TYPE>;
using DataType = std::array<VALUE_TYPE, SIZE>;
constexpr explicit ArrayND(const InitType& initializer_list)
: data(init<VALUE_TYPE, SIZE>(initializer_list))
{}
DataType data;
};
template<class VALUE_TYPE>
struct ArrayND<VALUE_TYPE, 1>
{
using InitType = std::initializer_list<VALUE_TYPE>;
using DataType = std::array<VALUE_TYPE, 1>;
constexpr explicit ArrayND(const InitType& initializer_list)
: data(init<VALUE_TYPE, 1>(initializer_list))
{}
DataType data;
};
And here's the usage in action:
constinit ArrayND<float, 1> my_scaler({11.11f});
constinit ArrayND<float, 3> my_2_array(
{ 0.1f, 0.2f, 0.3f }
);
constinit ArrayND<float, 2, 2> my_2x2_array(
{{ 0.1f, 0.2f }, { 0.3f, 0.4f }}
);
constinit ArrayND<float, 3, 2, 1, 2> my_3x2x5x7_array(
{
{{ {0.1f, 0.2f} }, { {0.3f, 0.4f} }},
{{ {0.1f, 0.2f} }, { {0.3f, 0.4f} }},
{{ {0.1f, 0.2f} }, { {0.3f, 0.4f} }}
}
);
Thanks for all the assistance guys! I still feel like there could be a way to do it with recursive variadic template parameters and constructor arguments... but this definitely gets me back on track and solves the problem I was trying to solve
Consider the following code:
struct A
{
// No data members
//...
};
template<typename T, size_t N>
struct B : A
{
T data[N];
}
This is how you have to initialize B: B<int, 3> b = { {}, {1, 2, 3} };
I want to avoid the unnecessary empty {} for the base class.
There is a solution proposed by Jarod42 here, however, it doesn't work with elements default initialization: B<int, 3> b = {1, 2, 3}; is fine but B<int, 3> b = {1}; is not: b.data[1] and b.data[2] aren't default initialized to 0, and a compiler error occurs.
Is there any way (or there will be with c++20) to "hide" base class from construction?
The easiest solution is to add a variadic constructor:
struct A { };
template<typename T, std::size_t N>
struct B : A {
template<class... Ts, typename = std::enable_if_t<
(std::is_convertible_v<Ts, T> && ...)>>
B(Ts&&... args) : data{std::forward<Ts>(args)...} {}
T data[N];
};
void foo() {
B<int, 3> b1 = {1, 2, 3};
B<int, 3> b2 = {1};
}
If you provide fewer elements in the {...} initializer list than N, the remaining elements in the array data will be value-initialized as by T().
Since C++20 you could use designated initializers in aggregate initialization.
B<int, 3> b = { .data {1} }; // initialize b.data with {1},
// b.data[0] is 1, b.data[1] and b.data[2] would be 0
Still with constructor, you might do something like:
template<typename T, size_t N>
struct B : A
{
public:
constexpr B() : data{} {}
template <typename ... Ts,
std::enable_if_t<(sizeof...(Ts) != 0 && sizeof...(Ts) < N)
|| !std::is_same_v<B, std::decay_t<T>>, int> = 0>
constexpr B(T&& arg, Ts&&... args) : data{std::forward<T>(arg), std::forward<Ts>(args)...}
{}
T data[N];
};
Demo
SFINAE is done mainly to avoid to create pseudo copy constructor B(B&).
You would need extra private tag to support B<std::index_sequence<0, 1>, 42> ;-)
I've found another solution that (I don't know how) works perfectly and solves the problem we were discussing under Evg's answer
struct A {};
template<typename T, size_t N>
struct B_data
{
T data[N];
};
template<typename T, size_t N>
struct B : B_data<T, N>, A
{
// ...
};
Here's a simple 2-dimensional C-style array example:
int c[2][2] = {
{ 1, 2 },
{ 3, 4 }
};
If I want this to be std::array, I have to use this:
std::array<std::array<int, 2>, 2> c = {
{ { 1, 2 } },
{ { 3, 4 } }
};
The declaration is more involved, and I have to use extra { } for initialization.
Is it possible to create a C-style array wrapper, with which I can do this?
my_array<int, 2, 2> c = {
{ 1, 2 },
{ 3, 4 }
};
So the declaration is simpler, and there is no need of the extra { }.
If creating something like this is possible, would this solution have some drawbacks compared to std::array?
I've managed to do this:
template <typename T, std::size_t ...S>
struct ArrayTypeHelper;
template <typename T, std::size_t HEAD, std::size_t ...TAIL>
struct ArrayTypeHelper<T, HEAD, TAIL...> {
using Type = typename ArrayTypeHelper<T, TAIL...>::Type[HEAD];
};
template <typename T, std::size_t HEAD>
struct ArrayTypeHelper<T, HEAD> {
using Type = T[HEAD];
};
template <typename T, std::size_t ...S>
struct my_array {
typename ArrayTypeHelper<T, S...>::Type data;
};
But this still needs extra { }:
my_array<int, 2, 2> b = { {
{ 1, 2 },
{ 3, 4 }
} };
The problem is that a c-array member is always viewed by the brace elision algorithm as a single entity. If the inner initializer begins with a brace , the algorithm expects an initializer list with one element. So the solution is to define the carray wrapper in such a way that the brace elision algorithm knows how many elements are aggregated by the wrapper.
To do this, the only solution I see is to simulate an array through multiple inheritance:
#include <utility>
using namespace std;
template<class T,size_t DeDuplicate>
struct holder{
T val;
};
template<class T,class IndSeq>
struct carray_wrapper_b;
template<class T,size_t...Is>
struct carray_wrapper_b<T,index_sequence<Is...>>
:holder<T,Is>...
{ };
template<class T,size_t I,size_t...Is>
struct make_carray_{
using type = carray_wrapper_b<typename make_carray_<T,Is...>::type
,make_index_sequence<I>>;
};
template<class T,size_t I>
struct make_carray_<T,I>{
using type = carray_wrapper_b<T,make_index_sequence<I>>;
};
template<class T,size_t...Is>
using carray_wrapper = typename make_carray_<T,Is...>::type;
carray_wrapper<int,2,2> x = { {1,2},{3,4}};
carray_wrapper<int,2,2> y = { 1,2,3,4};
This array wrapper can also be used for initialization purpose only:
template<class T,size_t I,size_t...Is>
struct carray{
carray(initializer_list<T>);
carray(initializer_list<carray_wrapper<T,Is...>>);
};
carray<int,2,2,3> arr{1,2,3,4,5,6,7};
carray<int,2,2,3> arr2{{1,2},{3,4}};
carray<int,2,2,3> arr3{{{1,2},{3,4}},{1,2}};
This is an interesting problem I'm thinking about a time ago.
Given a struct with an underlying aggregate:
#include <array>
template <typename T, size_t N>
struct A
{
constexpr A() = default;
template <typename ... Ts>
constexpr A(const T& value, const Ts& ... values); // magic
std::array<T, N> arr; // aggregate
};
How would you implement variadic template constructor A(const T& value, const Ts& ... values) to
accept both values of type T and another A<T, N>
properly initialize the underlying aggregate based on the values represented by the passed arguments
respect the capacity of the aggregate
support C++14 constexpr rules and do not introduce any runtime overhead
Satisfying the above requirements, it is possible to do the following:
int main()
{
A<int, 3> x(1, 2, 3);
A<int, 2> y(1, 2);
A<int, 6> a(x, 1, 2, 3);
A<int, 6> b(1, x, 2, 3);
A<int, 6> c(1, 2, x, 3);
A<int, 6> d(1, 2, 3, x);
A<int, 6> e(x, x);
A<int, 6> f(y, y, y);
return 0;
}
Here's one approach that works, but could almost certainly be improved on.
We have a constructor for A that takes a parameter pack, converts each element into a tuple, concatenates the tuples together to make for one large tuple, and then simply uses aggregate initialization from that big tuple. All of the following can be constexpr, I just omitted it for brevity.
First we do the conversion:
template <class... Us>
A(Us const&... us)
: A(std::tuple_cat(as_tuple(us)...))
{ }
With:
// single argument
template <class U>
auto as_tuple(U const& u) {
return std::forward_as_tuple(u);
}
// aggregate argument
template <size_t M>
auto as_tuple(A<T, M> const& a) {
return as_tuple(a, std::make_index_sequence<M>{});
}
template <size_t M, size_t... Is>
auto as_tuple(A<T, M> const& a, std::index_sequence<Is...> ) {
return std::forward_as_tuple(std::get<Is>(a.arr)...);
}
And then we just initialize from there:
template <class... Us, class = std::enable_if_t<(sizeof...(Us) <= N)>>
A(std::tuple<Us...> const& t)
: A(t, std::index_sequence_for<Us...>{})
{ }
template <class... Us, size_t... Is>
A(std::tuple<Us...> const& t, std::index_sequence<Is...> )
: arr{{std::get<Is>(t)...}}
{ }
Demo
The answer by #Barry is certainly correct and acceptable. But it requires some C++14 library additions (which you could probably also write yourself in C++11), and overall requires some good tuple- and meta-programming fu.
Let's view multiple arguments a "range of ranges", where a range is just a pointer and a size. Scalar arguments are just a size-1 range, and A<T, N> arguments are size-N ranges.
template<class T>
struct Range
{
T const* data_;
std::size_t size_;
constexpr T const* begin() const noexcept { return data_; }
constexpr T const* end() const noexcept { return data_ + size_; }
constexpr std::size_t size() const noexcept { return size_; }
};
template<class T>
constexpr Range<T> as_range(T const& t)
{
return { &t, 1 };
}
template<class T, std::size_t N>
struct A;
template<class T, std::size_t N>
constexpr Range<T> as_range(A<T, N> const& a)
{
return { a.arr, N };
}
You can then simply do a double loop over all elements of all ranges
template <typename T, size_t N>
struct A
{
T arr[N]; // aggregate
constexpr A() = default;
template <typename U, typename... Us>
constexpr A(U const u, Us const&... us)
:
arr{}
{
Range<T> rngs[1 + sizeof...(Us)] { as_range(u), as_range(us)... };
auto i = 0;
for (auto const& r : rngs)
for (auto const& elem : r)
arr[i++] = elem;
assert(i == N);
}
};
Live Example working at compile-time (requires GCC >= 6.0 or Clang >= 3.4)
template <class T, size_t N>
void print(A<T, N> const& a) {
for (T const& t : a.arr) {
std::cout << t << ' ';
}
std::cout << '\n';
}
int main()
{
constexpr A<int, 3> x(1, 2, 3);
constexpr A<int, 2> y(1, 2);
constexpr A<int, 6> a(x, 1, 2, 3);
constexpr A<int, 6> b(1, x, 2, 3);
constexpr A<int, 6> c(1, 2, x, 3);
constexpr A<int, 6> d(1, 2, 3, x);
constexpr A<int, 6> e(x, x);
constexpr A<int, 6> f(y, y, y);
print(a); // 1 2 3 1 2 3
print(b); // 1 1 2 3 2 3
print(c); // 1 2 1 2 3 3
print(d); // 1 2 3 1 2 3
print(e); // 1 2 3 1 2 3
print(f); // 1 2 1 2 1 2
}
In all the modern C++ compilers I've worked with, the following is legal:
std::array<float, 4> a = {1, 2, 3, 4};
I'm trying to make my own class that has similar construction semantics, but I'm running into an annoying problem. Consider the following attempt:
#include <array>
#include <cstddef>
template<std::size_t n>
class float_vec
{
private:
std::array<float, n> underlying_array;
public:
template<typename... Types>
float_vec(Types... args)
: underlying_array{{args...}}
{
}
};
int main()
{
float_vec<4> v = {1, 2, 3, 4}; // error here
}
When using int literals like above, the compiler complains it can't implicitly convert int to float. I think it works in the std::array example, though, because the values given are compile-time constants known to be within the domain of float. Here, on the other hand, the variadic template uses int for the parameter types and the conversion happens within the constructor's initializer list where the values aren't known at compile-time.
I don't want to do an explicit cast in the constructor since that would then allow for all numeric values even if they can't be represented by float.
The only way I can think of to get what I want is to somehow have a variable number of parameters, but of a specific type (in this case, I'd want float). I'm aware of std::initializer_list, but I'd like to be able to enforce the number of parameters at compile time as well.
Any ideas? Is what I want even possible with C++11? Anything new proposed for C++14 that will solve this?
A little trick is to use constructor inheritance. Just make your class derive from another class which has a pack of the parameters you want.
template <class T, std::size_t N, class Seq = repeat_types<N, T>>
struct _array_impl;
template <class T, std::size_t N, class... Seq>
struct _array_impl<T, N, type_sequence<Seq...>>
{
_array_impl(Seq... elements) : _data{elements...} {}
const T& operator[](std::size_t i) const { return _data[i]; }
T _data[N];
};
template <class T, std::size_t N>
struct array : _array_impl<T, N>
{
using _array_impl<T, N>::_array_impl;
};
int main() {
array<float, 4> a {1, 2, 3, 4};
for (int i = 0; i < 4; i++)
std::cout << a[i] << std::endl;
return 0;
}
Here is a sample implementation of the repeat_types utility. This sample uses logarithmic template recursion, which is a little less intuitive to implement than with linear recursion.
template <class... T>
struct type_sequence
{
static constexpr inline std::size_t size() noexcept { return sizeof...(T); }
};
template <class, class>
struct _concatenate_sequences_impl;
template <class... T, class... U>
struct _concatenate_sequences_impl<type_sequence<T...>, type_sequence<U...>>
{ using type = type_sequence<T..., U...>; };
template <class T, class U>
using concatenate_sequences = typename _concatenate_sequences_impl<T, U>::type;
template <std::size_t N, class T>
struct _repeat_sequence_impl
{ using type = concatenate_sequences<
typename _repeat_sequence_impl<N/2, T>::type,
typename _repeat_sequence_impl<N - N/2, T>::type>; };
template <class T>
struct _repeat_sequence_impl<1, T>
{ using type = T; };
template <class... T>
struct _repeat_sequence_impl<0, type_sequence<T...>>
{ using type = type_sequence<>; };
template <std::size_t N, class... T>
using repeat_types = typename _repeat_sequence_impl<N, type_sequence<T...>>::type;
First of what you are seeing is the default aggregate initialization. It has been around since the earliest K&R C. If your type is an aggregate, it supports aggregate initialization already. Also, your example will most likely compile, but the correct way to initialize it is std::array<int, 3> x ={{1, 2, 3}}; (note the double braces).
What has been added in C++11 is the initializer_list construct which requires a bit of compiler magic to be implemented.
So, what you can do now is add constructors and assignment operators that accept a value of std::initializer_list and this will offer the same syntax for your type.
Example:
#include <initializer_list>
struct X {
X(std::initializer_list<int>) {
// do stuff
}
};
int main()
{
X x = {1, 2, 3};
return 0;
}
Why does your current approach not work? Because in C++11 std::initializer_list::size is not a constexpr or part of the initializer_list type. You cannot use it as a template parameter.
A few possible hacks: make your type an aggregate.
#include <array>
template<std::size_t N>
struct X {
std::array<int, N> x;
};
int main()
{
X<3> x = {{{1, 2, 3}}}; // triple braces required
return 0;
}
Provide a make_* function to deduce the number of arguments:
#include <array>
template<std::size_t N>
struct X {
std::array<int, N> x;
};
template<typename... T>
auto make_X(T... args) -> X<sizeof...(T)>
// might want to find the common_type of the argument pack as well
{ return X<sizeof...(T)>{{{args...}}}; }
int main()
{
auto x = make_X(1, 2, 3);
return 0;
}
If you use several braces to initialize the instance, you can leverage list-init of another type to accept these conversions for compile-time constants. Here's a version that uses a raw array, so you only need parens + braces for construction:
#include <array>
#include <cstddef>
template<int... Is> struct seq {};
template<int N, int... Is> struct gen_seq : gen_seq<N-1, N-1, Is...> {};
template<int... Is> struct gen_seq<0, Is...> : seq<Is...> {};
template<std::size_t n>
class float_vec
{
private:
std::array<float, n> underlying_array;
template<int... Is>
constexpr float_vec(float const(&arr)[n], seq<Is...>)
: underlying_array{{arr[Is]...}}
{}
public:
constexpr float_vec(float const(&arr)[n])
: float_vec(arr, gen_seq<n>{})
{}
};
int main()
{
float_vec<4> v0 ({1, 2, 3, 4}); // fine
float_vec<4> v1 {{1, 2, 3, 4}}; // fine
float_vec<4> v2 = {{1, 2, 3, 4}}; // fine
}
Explicitly specify that the data type for the initialization to floating point type. You can do this by doing "1.0f" instead of putting "1". If it is a double, put "1.0d". If it is a long, put "1l" and for unsigned long put "1ul" and so on..
I've tested it here: http://coliru.stacked-crooked.com/a/396f5d418cbd3f14
and here: http://ideone.com/ZLiMhg
Your code was fine. You just initialized the class a bit incorrect.
#include <array>
#include <cstddef>
#include <iostream>
template<std::size_t n>
class float_vec
{
private:
std::array<float, n> underlying_array;
public:
template<typename... Types>
float_vec(Types... args)
: underlying_array{{args...}}
{
}
float get(int index) {return underlying_array[index];}
};
int main()
{
float_vec<4> v = {1.5f, 2.0f, 3.0f, 4.5f}; //works fine now..
for (int i = 0; i < 4; ++i)
std::cout<<v.get(i)<<" ";
}