On Sat, Jan 24, 2026 at 7:38 AM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
Hi All,
Thank you, Andrzej, for the continuous feedback. "Restrictions" (lazy arrays) in Boost.Multi look like a useful and
practical component. The below feedback may sound negative, but it should be treated as suggestions for improving the feature.
I find the name "restriction" surprising. Is this a term of art? "Lazy array" immediately conveys the idea, so I wonder why you decided to go with "restriction".
TL;DR: Using the operator^ is inspired by the "restriction" operator in mathematics, which is either a vertical line (sorry, pipe was already taken) or an *up arrow*, hence op^ (wedge). https://en.wikipedia.org/wiki/Restriction_(mathematics) Long answer: Historically, I started this feature from a private interaction with Sean Parent, in which he said "... You might also look at what would be required to do initialization on construction - for example, *being able to provide generators or transform functions from coordinate to value*." That is, provide a way to map from i,j,k to an element. I interpreted that one wanted to do this in a fundamental way, meaning without late assignment. I hacked something into the constructor, but I was not happy with it because it would not work efficiently with (array) assignment. Hence, I invented this feature; it took me a while to get it right. Sean answered late: "*I like the use of `^` for restrictions *- is this something that can also be piped as a range? `[](auto i, auto j){ return i + j; } ^ multi::extensions_t(3, 4) | column | reverse` -" (he later asked for "infinitely" extended restrictions!). Since the start, I've tried to avoid lazy arrays, lazy expressions, and similar constructs, because I believe they ultimately conspire against neat library designs (see Blitz or Eigen). I think the difference here is that they don't affect the core design of allocated arrays. that remain fundamental. Now, to answer your question, I'm going to paste my answer to Sean's request: I considered using constructors from function objects/lambdas, as well as many other options. But I achieved something more interesting, which is to create lazy multidimensional arrays based on functions defined in a grid of valid coordinates. I call these objects "restrictions," and they are generated from a function object and an extension object from the library ``` auto restriction = [](auto i, auto j) { return i + j; } ^ multi::extensions_t(3, 4); ``` *Using the operator^ is inspired by the "restriction" operator in mathematics, which is either a vertical line (sorry, pipe was already taken) or an *up arrow*, hence op^.* https://en.wikipedia.org/wiki/Restriction_(mathematics) This seems like pure syntactic sugar, but it is not. The restriction object behaves like an array container ("range") in the library as much as it can: it has .begin(), .end(), .size(), extensions(), and .elements() (flatten access). In turn, this allows the use of algorithms (even parallel ones) that can be used for construction (through uninitialized_copy_n) or assignment (copy_n). (It behaves like a supercharged multidimensionally transformed iota) This actually achieves what you were asking for, for example, in construction ``` multi::array<double, 2> A( [](auto i, auto j) { return i + j; } ^ multi::extensions_t(3, 4) ); ``` To clarify, this has the same effect as ``` multi::array<double, 2> A( {3, 4} ); auto [is, js] = A.extensions(); for(auto i : is) for(auto j : js) A[i][j] = i + j; ``` But directly on the construction of A through proper algorithms, (and A can be const, and no raw loops) (both versions are compared here: https://godbolt.org/z/K87ETE18K) These restrictions can do much more, including acting efficiently on assignments ``` A = [](auto i, auto j) { return i + j; } ^ multi::extensions_t(3, 4); // calls copy(rhs.elements().begin(), ...end(), lhs.elements().begin()); allocates if necessary // doesn't call allocate if not necessary! ``` or ``` A == [](auto i, auto j) { return i + j; } ^ multi::extensions_t(3, 4); // calls equal(rhs.elements().begin(), ... end(), lhs.elements().begin()) ``` (Note that the library can dispatch to specialized algorithms, so in GPUs, copy or equal algorithms are parallelized. In other words, these are not necessarily nested loops, which would be terrible. Additionally, restrictions can interact directly with STL algorithms; they don't need intermediate explicitly allocated arrays. Here is the code to play in Godbolt:https://godbolt.org/z/7fb3ajohP Feel free to criticize the design or let me know if it's not headed in the right direction. End of quote.
I already complained that the "Reference" section is not serving its purpose well. Regarding "restrictions", they are simply missing from that section. Having read the tutorial, and wanting to use the feature in my program, I need to know:
You are right; I find writing a formal reference for this part of the library extremely difficult, mainly because it is syntactic and conceptual, and there is little value in specifying classes and member functions. I am willing to try, though.
1. Which header to include
#include <boost/multi/restriction.hpp>
2. What is the type returned by the "wedge" operator
The type is `boost::multi::restriction<2, Functor>`; there is no type erasure. I am not sure whether to make this type an implementation detail, since I didn't find good uses for naming this type. Let alone, I cannot give many guarantees if the Functor instance is not a "pure" function, deterministic, and without side effects. Side note: It seems that one can use the entire library by mentioning only one type `multi::array`. If so, I would be proud of it. (across compilation boundaries, I agree that `subarray` could be useful to be mentioned too).
3. Is the return type something that I should be able to name, use? Or is the contract "just assign the result to your array, and don't ask for the type name"?
That seems to be the case; promoting any other use opens a can of worms. 4. What are the preconditions of the operation?
None? If the function is pure noexcept and has a wide domain contract. Anything outside this can be regarded as a precondition.
5. Are there things that I can do wrong when using the function and resulting types?
It really boils down to the Functor body. If you remain in the "pure" domain, nothing can go wrong; restrictions don't allocate or depend on any external conditions. Of course, an assignment can fail in a variety of ways; allocations could fail, for example. But remember that assignment is a function that `array` or `subarray` controls, not `restriction`. 7. Does the function that I provide need to have the same number of
arguments as the number of dimensions in the `extents` that I provide?
That is a good question. I think it has to have at least some argument dimensions. (Otherwise, the .elements() function will generate something useless). It can have other overloads. One thing I have been debating is whether I should use overloads with fewer arguments (specifically, one). In this way, the Functor can express optimizations, although they have to be semantically correct. It could also be challenging for the user to get this right.
8.Do you guarantee that the index values that will be passed to subsequent invocations of my function will come in specific order? Or do I need to make sure that my function is pure/regular?
Let's put it this way: if you make sure that your function is pure/regular, you are golden. Anything outside this can be powerful and dangerous; it falls into the category "know what you are doing". This is nothing exotic: for example, at the moment, nothing prevents you from writing a random matrix generator like this, if you know what you are doing (to begin, not care in which order the indices are evaluated). ``` multi::array<int, 2> arr = [](auto...) { return rand(); }; ``` 9. Does the function that I provide need to be copyable?
Doesn't need to be copyable in general. (I will probably ask for it to be copyable if you want to copy the restriction itself, which also implies that the restriction that you are using has a state.) This also brings up the topic of "constness" of restrictions. They should apply to the functor itself IMO.
10. Header `restriction.hpp` has functions `make_restriction()` defined. Are they intended for users? If so, they need to be listed in "Reference"' also.
I don't remember.
I question the choice of using an arbitrary operator for creating lazy arrays. It makes the user code difficult to read and understand. I imagine that in my company I assign a junior developer to fix something in the code, and they are exposed to these bitwise-xor operators. This is an unnecessary confusion. A named factory function like `make_lazy` would immediately sell the intent: it combines the extents with an "indexes-to-value" function to produce a lazy array.
Thank you for the suggestion. Do you question the availability of this feature? (Take into account that it is completely opt-in; you need to include a header for this.) Or do you question the name? (My concern is that, like with the word "view", "lazy" can mean anything these days. )
It looks like there is an implied concept in your library, "non-mutable array access"., which probably includes being the RHS of the assignment to a "mutable array access". Now I feel even stronger that it should be given a name and be defined, even if semi-formally.
Yes, the main reason for not defining concepts is that 1) there are many concepts from ranges that already can be used, and I didn't want to introduce near (not subsumed) duplicates, 2) I will probably get it wrong in the first attempt, and 3) since the library is C++17, I wasn't able to dogfooding it. (One thing that I find ironic is that most of the concepts I can think of already exist in one way or another, and the main syntax that introduces new concepts is ".elements()". )
The tutorial for restrictions mentions the function ".home()". Is this intended to be part of the library's interface? If so, it must appear in the "Reference" section. The Reference section defines what is the intended interface of the library as opposed to declarations that happen to exist in the headers for other reasons: as temporary solutions, implementation details, experiments or artifacts.
Good catch, .home() is very fundamental to the design. It is what allows us to write CUDA kernels in a sane way. (it is a context where you can only pass by --trivial-- value). Thanks, Alfredo