Thank you, Andrzej, for the feedback and the appreciation of the library and the effort. On Fri, Jan 9, 2026 at 3:52 PM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
I would like to request a Design Rationale section that would address some of the questions up front and bring some insight into the domain:
I wrote a design rationale section based on your questions. https://correaa.gitlab.io/boost-multi/multi/technical.html
1. Is it a common need in the mentioned application fields to add a new dimension to the already stored N-dimensional data? If so, could we see an example of how to do it?
No, it is not common to add dimensions on the fly. A new object with different dimensionality can be idiomatically created in this way: `multi::array<T, D> arr =...; `multi::array<T, D+1> extrude(n, arr);` https://godbolt.org/z/GxKscxsh6 2. Why is c++17 chosen as the minimum?
The main reason is the use of `if constexpr`, it appears 68 times Earlier standards (C++14 and below) would require Heavier template metaprogramming and much more boilerplate. I can work out a C++14-compatible version if there is demand.
3. The number of dimensions is static, but the sizes in each dimension are dynamic. Can you provide the rationale for this choice?
The dimensionality (number of dimensions) is static because it is useful to know at compile time. The sizes in each dimension are dynamic because it is useful to choose these numbers at runtime. Empty array values are useful too, and eventually they are resized (reextended) to the correct size. Other choices include runtime dimensions, compile-time sizes, or runtime-fixed sizes. This goes against the library's philosophy; it is useful to dispatch arrays to functions that know their dimensionality at compile time. Algorithms that work the same way for different dimensions are basically nonexistent. Libraries that make dimensionality runtime-dependent need to check it at runtime. Multi achieves a different kind of flexibility (the right kind), but a) treating ND arrays themselves as, for example, 1D arrays of N-1 D subarrays or N - 1D arrays of 1D subarrays, or anything in between. b) providing .elements() which gives a flattened view of an array of any dimensionality.
The design:
1. I am still trying to digest the way by-reference and by-value is handled in the library in the presence of the "ref"/"view" types. It looks like the simple advice for the new users would be: * Always use `auto& ref = x` if you need the reference semantics, where `x` is whatever expression.
The language is fighting me hard here. It is actually `auto&& ref = x` (I wish it were `auto& ref = x` allowed and I wish the language prevented this `auto ref = x`, but it doesn't). * Always use `auto val = +x` if you need value semantics, where `x` is any
expression. It looks like it is optimum even when `x` is of type `array`.
To be generically consistent, I have to make the return type of unary '+' semantically consistent. This means that +x returns a copy of x unconditionally. So it has a cost when x is an array (allocation and copy of elements), and then that is moved (materialized?) into the auto variable; the copy was needed anyway. So, yes, it is also optimal when `x` is a plain array. (I could make unary '+' return `multi::array const&` but I think that broke something fundamental. We can investigate more this. Maybe I missed something.) At least for r-values, the recommendations are still correct and not inefficient. ``` class multi::array { ... auto operator+() const& { return multi::array{*this}; } constexpr auto operator+() && { return multi::array{std::move(*this)}; } ``` I included this explicit advice at the end of this section of the tutorial: https://correaa.gitlab.io/boost-multi/multi/tutorial.html#tutorial_copy_assi...
2. The initialization from values is too similar to the initialization from extents. I cannot easily figure out which initialization is used in `multi::array a({3, 4, 5});`. This is error-prone. I would prefer a longer: `multi::array a(multi::extents{3, 4, 5});`. On the other hand, does the initialization from small manual static sets of data happen often? I would think that for these use cases you would read the data from some external source.
I would claim that the problem with `multi::array a({3, 4, 5});` is the use of CTAD in the first place. If you don't use CTAD, you are forced to make it clear: `multi::array<int, 1> a({3, 4, 5});` `multi::array<int, 3> a({3, 4, 5});` Although for the former I recommend multi::array<int, 1> a = {3, 4, 5};` Yes, using the type `extensions_t` makes everything more explicit, and it is completely opt-in. CTAD doesn't work in your proposed `multi::array a(multi::extensions_t<3>{3, 4, 5});` because there is no information about the type of the element, fixes are (choose one) ``` multi::array<int, 3> a(multi::extensions_t<3>{3, 4, 5}); // recommended multi::array<int, 3> a(multi::extensions_t{3, 4, 5}); // uses CTAD inside multi::array a(multi::extensions_t{3, 4, 5}, int{}); // double CTAD, but weird ``` This is a summary of the possibly confusing cases: https://godbolt.org/z/K3KoYbr88
3. If the docs recommend that all functions should be generic to account for subarrays and refs, then this library could offer a concept like `Indexable<Dimensionality, ElementType>` available for C++20 compilers.
I agree, the most difficult part is to make this concept, also how to include (or not) the notion that is Mutable, or Reextendable (resizable).
4. Are sizes equal to strands? Does .elements() offer contiguous access?
I don't know what are strands, do you mean strides. Not, in general. For contiguous arrays in memory, strides can be trivially computed from the array sizes. For example in D = 2, given size1 and size2 (columuns and row) stride1 = size2, stride2 = 1
5. For the initialization, did you consider factory functions or tag parameters? ( https://akrzemi1.wordpress.com/2016/06/29/competing-constructors/). The differentiation of meaning by braces/parens seems easy to misinterpret.
Yes, I considered it. But I have to find a case in which it can really be a problem. The existence of multi::extensions_t<D> as first class type in the library makes the disambiguation much simpler than in the case of std::vector. So, in some sense, multi::extensions_t<D> is my "tag" and resolves all the cases as far as I know.
6. All the subarrays and dynamic_arrays have function swap, but it is not documented. Does it have a precondition that sizes should match?
Good catch, swaperator is a friend. It is now documented. Yes, for subarrays references (including dynamic_arrays) sizes must match, and the operation is O(n) For array values, sizes don't need to match, and swap is O(1).
Documentation:
1.It is very clear already!
Thank you!
2. In the introduction it is worth explaining the term "stride-based layout" or link to the definition. This concept seems crucial to the library.
Linked to Wikipedia article.
3. The docs say "or tensors in the 3D case". Tensors are arbitrary dimension.
You are technically correct. I fixed it to "(althoug they are still good building blocks for implementing mathematical algorithms, such as representing algebraic dense matrices in the 2D case, or tensors in the general case.)"
4. I think that in real, practical use cases the array data is populated from files or datastres rather than from literals (compile-time values in braces). Could the tutorial demonstrate the endorsed way of doing such data population with this library?
yes, - (de)serialization for internal (to the library) transactions https://correaa.gitlab.io/boost-multi/multi/interop.html#interop_serializati... - lazy arrays for programmatically: https://correaa.gitlab.io/boost-multi/multi/tutorial.html#tutorial_restricti... Since there is no single way to represent arrays in the wild, I don't want to impose one. I will talk to the Boost. JSON people to come up with a way to do JSON that is not intrusive and doesn't add dependencies. 5. I really appreciate that you prefix names with `multi::`. The examples
could also mention the headers that you need to include. The initial docs examples may be the place where I will learn the headers.
Good idea, each section in the tutorial introduces a specific header for the functionality: - array_ref.hpp (subarrays and references to non-owning memory) - array.hpp (dynamic_array, array, owning memory) - restriction.hpp (lazy arrays) - broadcast.hpp (laze expressions)
6. The interface discoverability is not great: I wanted to quickly check the contract of `array::elements()`. I cannot find it. The reference section only has repeated text "same as for dynamic_array" (no link). Generally, the "redirect" philosophy in the reference documentation is mean.In order to look up the semantics of array::operator(), I am redirected: first from `array` to `dynamic_array`, then to `array_ref`, and only then to `subarray`.
What do you mean by contract? The search box leads me to: "a flatted view of all the elements rearranged canonically. A.elements()[0] → A[0][0], A.elements()[1] → A[0][1], etc. The type of the result is not a subarray but a special kind of range. Takes no argument." what other info do you want to see? exceptions handling, allocations? Yes, I should repeat all the members for arrays, given how important it is, and once each member is spelled out, I can redirect directly to the base class that implements it. This will take time.
7. Generally, we need to see a more detailed Reference section. For instance, what is the return type of operator[]?
Searching for "operator[]" leads me to "access specified element by index (single argument), returns a `reference` (see above), for D > 1 it can be used recursively" `reference` is a member type, looking above it says "multi::subarray<T, D-1> or, for D == 1, pointer_traits<P>::reference (usually T&)" I will add "... return a `reference` member type (see above), ..."
8. Document if `array` and siblings can have dimensionality 0 (which would mean access to a single element).
yes, it can but it can be confusing in the documentation. They potentially have very weird properties. They can have 1 or be in a partially formed state (I didn't commit to what is the non-empty state) https://godbolt.org/z/fGbEbvM74
9. It is strange to see a section titled "Static arrays" that describes type `dynamic_array`. In C++ "static" and "dynamic" tend to have opposite meanings.
That is a relic; `dynamic_array` was called `static_array`. `dynamic_array` is a better name because it has a dynamic size. Fixed. Thank you, Alfredo