0

I have been devloping a class template for a stack-allocated N-dimensional array (note that I have not yet implemented support for >1-D arrays):

template <typename T, std::size_t... DIMS> class ndarray {
  static constexpr std::size_t NDIM{sizeof...(DIMS)};
  static constexpr std::array<T, NDIM> SHAPE{DIMS...};
  static constexpr std::size_t SIZE{(1 * ... * DIMS)};

  std::array<T, SIZE> _data;

public:
  constexpr ndarray() : _data{} {}

  constexpr ndarray(T element) : _data{element} {}

  constexpr ndarray(const T (&elements)[SIZE])
      : _data(std::to_array(elements)) {}

  constexpr ndarray(const std::array<T, SIZE> &elements) : _data(elements) {}

  constexpr std::size_t size() { return SIZE; }

  constexpr std::array<T, NDIM> shape() { return SHAPE; }

  constexpr ndarray &operator=(const T (&elements)[SIZE]) {
    _data = std::to_array(elements);
  }
};

template <typename T, std::size_t N>
ndarray(const T (&elements)[N]) -> ndarray<T, N>;

The above code allows for every method of initialization and assignment I am looking for except one:

// 0-D arrays
void zero() {
  int z{1};
  int x[1];

  ndarray a(1);
  ndarray b{1};
  ndarray c = 1;
  ndarray<int> d;
  d = 1;

  ndarray e(z);
  ndarray f{z};
  ndarray g = z;
  ndarray<int> h;
  h = z;
}

// 1-D arrays
void one() {
  int y[]{1, 2};
  std::array z{1, 2};

  ndarray a({1, 2});
  ndarray b{{1, 2}};
  ndarray c = {1, 2};
  ndarray<int, 2> d;
  d = {1, 2};

  ndarray e(y);
  ndarray f{y};
  ndarray g = y;
  ndarray<int, 2> h;
  h = y;

  ndarray i(z);
  ndarray j{z};
  ndarray k = z;
  ndarray<int, 2> l;
  l = z;
}
error C2641: cannot deduce template arguments for 'ndarray'
error C2780: 'ndarray<T,N> ndarray(const T (&)[N])': expects 1 arguments - 2 provided
note: see declaration of 'ndarray'
error C2780: 'ndarray<T,DIMS...> ndarray(const std::array<T,ndarray<T,DIMS...>::SIZE> &)': expects 1 arguments - 2 provided
note: see declaration of 'ndarray'
error C2780: 'ndarray<T,DIMS...> ndarray(const T (&)[ndarray<T,DIMS...>::SIZE])': expects 1 arguments - 2 provided
note: see declaration of 'ndarray'
error C2780: 'ndarray<T,DIMS...> ndarray(T)': expects 1 arguments - 2 provided
note: see declaration of 'ndarray'
error C2780: 'ndarray<T,DIMS...> ndarray(void)': expects 0 arguments - 2 provided
note: see declaration of 'ndarray'
error C2780: 'ndarray<T,DIMS...> ndarray(ndarray<T,DIMS...>)': expects 1 arguments - 2 provided
note: see declaration of 'ndarray'

The copy-list-initialization of c in one is the cause of the compiler error. ndarray c = {{1, 2}} works, but this worries me because it is easily confused with an array of shape [1, N]. It also means that ndarray foo = 1 is equivalent to ndarray foo = {1}, which I strongly dislike.

Unfortunately, I cannot use either std::initializer_list or parameter pack constructor, as they would lead to some ambiguous cases (would ndarray foo{1} have shape [] or [1]?). The only option I have thought of is to make the constructors explicit to prevent copy-list-initialization entirely, although doing this would limit my class's interoperatibility with its contained type to some extent. Is there another way around this?

Edit: I just realized that ndarray foo({1}) is also ambiguous as it stands. Very frustrating.

QuaternionsRock
  • 832
  • 7
  • 14

1 Answers1

1

Fixing ndarray c = initilization

You can initialize the std::array with double braces:

reference: How to construct std::array object with initializer list?

// 1-D arrays
void one() {
  int y[]{1, 2};
  std::array z{1, 2};

  ndarray a({1, 2});
  ndarray b{{1, 2}};
  ndarray c = {{1, 2}}; // double braces
}

Fixing your error C2641: cannot deduce template arguments for 'ndarray'

By adding move constructor and reference constructor I'm able to compile your code:

constexpr ndarray(const T& element) : _data{element} {}
constexpr ndarray(const T&& element) : _data{element} {}

Fixing ambiguous ndarray foo({1})

if you want to make ndarray foo({1, 2}) create an array of 2 elements you must declare a constructor which takes in parameter a C array as follows:

constexpr ndarray(const T elements[]) {
  _data = reinterpret_cast<std::array<int, SIZE>&>(elements);
}
loic.lopez
  • 2,013
  • 2
  • 21
  • 42
  • How does the your third fix work, exactly? Shouldn't `const T elements[]` immediately decay into a pointer, which would make the array be of the type `array`? – QuaternionsRock Nov 12 '21 at 15:39
  • I have updated the third fix by adding a `reinterpret_cast`, I have tested it with c++20 and Visual Studio 2022 – loic.lopez Nov 12 '21 at 19:22
  • I still have a few issues with your answer. As of C++17, [this deduction guide](https://en.cppreference.com/w/cpp/container/array/deduction_guides) means that `std::array foo = {1, 2};` is now valid. If possible, I would like to avoid requiring double braces as your fix suggests, but this particular deduction guide strategy doesn't seem to be applicable to my class. Perhaps there is another that I can use? – QuaternionsRock Nov 15 '21 at 15:35
  • Also, your third fix doesn't seem to do anything about the ambiguity of `ndarray foo({1})`. – QuaternionsRock Nov 15 '21 at 15:53
  • @QuaternionsRock Maybe you can try something as described here: [How to construct std::array object with initializer list?](https://stackoverflow.com/a/6894191/7047493) – loic.lopez Nov 15 '21 at 20:30