4

Background

Suppose that I am trying to implement a fixed-size multi-dimensional array using a flat array:

template <class T, std::size_t... Dims>
struct multi_array {
    static constexpr std::size_t size() noexcept
    {
        return (Dims * ... * std::size_t{1});
    }
    std::array<T, size()> _elems;
};

The _elems member is made public to enable aggregate initialization for non-copyable, non-movable types: (suppose that non_movable has an explicit (int) constructor)

multi_array<non_movable, 2, 3> arr {
    non_movable(0), non_movable(1), non_movable(2),
    non_movable(3), non_movable(4), non_movable(5)
};

This compiles thanks to C++17 guaranteed copy elision — the corresponding elements of _elems are directly initialized from the unmaterialized prvalues, without requiring move constructors.

Problem

Now the problem is: in the above declaration, the multi-dimensional array is initialized like a one-dimensional array. I'll refer to this as "flat initialization", in contrast to "nested initialization":

multi_array<non_movable, 2, 3> arr {
    { non_movable(0), non_movable(1), non_movable(2) },
    { non_movable(3), non_movable(4), non_movable(5) }
}; // error: too many initializers for 'multi_array<non_movable, 3, 2>'

How can we enable nested initialization without having to change the underlying container used to implement multi_array from a one-dimensional array to a multi-dimensional array?

I guess that this would require a custom constructor, but I have no idea how to pass unmaterialized prvalues "transparently" through constructors. All I can think of is constructing a parameter out of them and then moving from the parameter, which doesn't work for non-movable types.

Minimal reproducible example

#include <array>
#include <cstddef>

struct non_movable {
    explicit non_movable(int) {}
    non_movable(const non_movable&) = delete;
    non_movable(non_movable&&) = delete;
    non_movable& operator=(const non_movable&) = delete;
    non_movable& operator=(non_movable&&) = delete;
    ~non_movable() = default;
};

template <class T, std::size_t... Dims>
struct multi_array {
    static constexpr std::size_t size() noexcept
    {
        return (Dims * ... * std::size_t{1});
    }
    std::array<T, size()> _elems;
};

int main()
{
    multi_array<non_movable, 3, 2> arr {
        non_movable(0), non_movable(1), non_movable(2),
        non_movable(3), non_movable(4), non_movable(5)
    };
    // multi_array<non_movable, 3, 2> arr {
    //     { non_movable(0), non_movable(1), non_movable(2) },
    //     { non_movable(3), non_movable(4), non_movable(5) }
    // };
    (void)arr;
}

(live demo)

L. F.
  • 19,445
  • 8
  • 48
  • 82

1 Answers1

3

I have no idea how to pass unmaterialized prvalues "transparently" through constructors.

What you want is not generally possible. Once a prvalue is passed as an argument to a function, it will either initialize an object parameter or it will manifest a temporary that is bound to a reference parameter. Either way, it stops being a prvalue.

The best you can do for your specific usecase is to keep it as an aggregate and either just accept that you have to initialize it as a "flat" multidimensional array, or make it a true multidimensional array by storing an array within an array. Both options have tradeoffs.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982