I want to make an NDArray
template which has a fixed dimension, but can be resized across each dimension.
My question is how to make it be able to deduce the dimensions in the constructor according to how many pair of {}
is used? The elements in the constructor will be used to initialize some of the elements.
#include <array>
#include <iostream>
template<typename T, size_t Dimension>
class NDArray
{
T* buffer = nullptr; //flattened buffer for cache locality
std::array<size_t, Dimension> dimension; //keep the current sizes of each dimension
public:
NDArray(std::initializer_list<T> elements) : dimension{elements.size()} //for 1D
{
std::cout << "Dimension = " << Dimension << '\n';
}
NDArray(std::initializer_list<NDArray<T, Dimension-1>> list) //how to make this works???
{
std::cout << "Dimension = " << Dimension << '\n';
}
};
template<typename T, size_t N>
NDArray(const T(&)[N]) -> NDArray<T, 1>;
int main()
{
NDArray a{ {3,4,5} };//OK, NDArray<int, 1> because of the deduction guide
NDArray b{ {{1,2,3}, {4,5,6}} };//Nope, I want: NDArray<int, 2>
}
Declaring a multi-dimensional array is similar to the one-dimensional arrays. For a 2D array, we need to tell C that we have 2 dimensions. This example declares a 2D integer array: A valid type is required (in this case int), followed by the name of the array, and the size of each dimension.
Total number of elements that can be stored in a multidimensional array can be calculated by multiplying the size of all the dimensions. For example: The array int x[10][20] can store total (10*20) = 200 elements. Similarly array int x[5][10][20] can store total (5*10*20) = 1000 elements.
Two – dimensional array is the simplest form of a multidimensional array. A two – dimensional array can be seen as an array of one – dimensional array for easier understanding. data_type [] [] array_name = new data_type [x] [y]; For example: int [] [] arr = new int [10] [20];
To iterate through all the elements of the multidimensional array we need to use nested for loop concept as below: NOTE: The inner arrays of the multidimensional array can be of variable length which makes it different from other programming languages multidimensional array where the length or columns are fixed.
This is impossible in the general case, but possible in however many specific cases you want to spell out.
An initializer list has no type. The only way you can deduce a type for it (as in, separate from having a default template argument) is that we have two special cases spelled out in [temp.deduct.call]/1:
If removing references and cv-qualifiers from
P
givesstd::initializer_list<P′>
orP′[N]
for someP′
andN
and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list independently, takingP′
as separate function template parameter typesP′i
and thei
th initializer element as the corresponding argument. In theP′[N]
case, ifN
is a non-type template parameter,N
is deduced from the length of the initializer list. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context ([temp.deduct.type]).
This is the rule that lets the following work:
template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }
static_assert(f({1, 2, 3}) == 1);
But that isn't enough to get this to work:
static_assert(f({{1, 2}, {3, 4}}) == 1); // ill-formed (no matching call to f)
Because the rule is - okay, we can strip one layer of initializer_list
but then we have to deduce the elements. And once we strip one layer of initializer list, we're trying to deduce T
from {1, 2}
and that fails - we can't do that.
But we know how to deduce something from {1, 2}
- that's this same rule. We just have to do it again:
template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }
template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<T>>) { return 2; }
static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
and again:
template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }
template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<T>>) { return 2; }
template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<std::initializer_list<T>>>) { return 3; }
static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
static_assert(f({{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}) == 3);
The same way we have the carve-out for std::initializer_list<T>
, we also have the carve-out for T[N]
. That works the same way, just a bit less typing:
template <typename T, size_t N>
constexpr auto f(T(&&)[N]) -> int { return 1; }
template <typename T, size_t N1, size_t N2>
constexpr auto f(T(&&)[N1][N2]) { return 2; }
template <typename T, size_t N1, size_t N2, size_t N3>
constexpr auto f(T(&&)[N1][N2][N3]) { return 3; }
static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
static_assert(f({{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}) == 3);
You may achieve almost what you want, if you are OK with explicitly creating NDArray inside the std::initialize_list
this way:
int main()
{
NDArray a{3,4,5}; // would be deduced to NDArray<int, 1>
NDArray b{ NDArray{1,2,3}, {4,5,6} }; // would be deduced to NDArray<int, 2>
}
Note that it is enough to explicitly add NDArray only for the first appearance of each dimension. For example, for 3D NDArray:
NDArray c { NDArray{ NDArray{8, 3}, {1, 2}, {1, 2, 3} },
{ {4, 5}, {8, 9, 7}, {2, 5} } };
To achieve that you need to have these two deduction guides:
template<typename T>
NDArray(const std::initializer_list<T>&)
-> NDArray<T, 1>;
template<typename T, size_t DIMENSIONS>
NDArray(const std::initializer_list<NDArray<T, DIMENSIONS>>&)
-> NDArray<T, DIMENSIONS + 1>;
Code example: http://coliru.stacked-crooked.com/a/1a96b1eaa0717a67
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With