N-dimensionally enclosed metalwork with patterns

I am trying to make N-dimensionally nested metalops with a metaprogramming pattern. The nested part is trivial, however, passing in an arbitrary number of iterative indices, since the template parameters in the innermost loop seem problematic.

A simple unnamed metaloop looks like this:

template <size_t I, size_t N> struct meta_for { template <typename Lambda> inline meta_for(Lambda &&iteration) { iteration(I); meta_for<I+1, N> next(static_cast<Lambda&&>(iteration)); } }; template <size_t N> struct meta_for<N, N> { template <typename Lambda> inline meta_for(Lambda &&iteration) { return; } }; #include <iostream> int main() { meta_for<0, 10>([&](size_t i) // perform 10 iterations { std::cout << i << '\n'; }); return 0; } 

Now I want to make metaloop that takes an N parameter denoting a dimension (nesting level) using:

 #include <iostream> int main() { // perform 3 dimensionally nested iterations // each index goes from 0 to 10 // so 10x10x10 iterations performed meta_for<3, 0, 10>([&](size_t i, size_t j, size_t k) { std::cout << i << ' ' << j << ' ' << k << '\n'; }); return 0; } 
+6
source share
2 answers

Someone who is better at this can improve my answer.

Live demo

The bottom line of my solution is that you declare sizes N with start and end.

It repeats the measurements of N-1 with the same beginning and end.

When he reaches the 1st dimension, he will actually begin to increase the beginning, causing the transferred function.

He will always try to pass several arguments that are identical to the number of dimensions (their indices).

So, the call is:

 meta_for<2, 0, 2>::loop( [](size_t i, size_t j) { std::cout << i << " " << j << std::endl; }); 

The result will look like this:

0 0

0 1

ten

eleven

Here's the meta_for structure using the helper, iterate :

 template<size_t D, size_t B, size_t E> struct meta_for { template<typename Func> static void loop(Func&& func) { iterate<D, B, B, E>::apply(std::forward<Func>(func)); } }; 

And helpers:

 // a helper macro to avoid repeating myself too much #define FN template<typename Func, typename... Args> \ static void apply(Func&& func, Args&&... a) // Outer loop. S="Self" or "Start". Indicating current index of outer loop. Intent is to iterate until S == E template<int Dim, size_t S, size_t B, size_t E> struct iterate { static_assert(S < E && B < E, "Indices are wrong"); FN { // outer loop recursive case. Recurse on lower Dimension (Dim-1), and then increment outer loop (S+1) iterate<Dim-1, B, B, E>::apply (func, a..., S); iterate<Dim, S+1, B, E>::apply (func, a...); } }; // Outer loop base case template<int Dim, size_t B, size_t E> struct iterate<Dim, E, B, E> { FN { // outer loop base case, End == End. Terminate loop } }; // innter loop. "S" is outer loop current index, which we need to pass on to function // "B" is inner loop (this loop) current index, which needs to iterate until B == E template<size_t S, size_t B, size_t E> struct iterate<1, S, B, E> { static_assert(S < E && B < E, "Indices are wrong"); FN { // inner loop recursive case. Perform work, and then recurse on next index (B+1) func(a..., B); iterate<1, S, B+1, E>::apply(func, a...); } }; // inner loop base case template<size_t S, size_t E> struct iterate<1, S, E, E> { FN { // inner loop base case, End == End. Terminate loop } }; // case where zero dimensions (no loop) template<size_t S, size_t B, size_t E> struct iterate<0, S, B, E> { static_assert(sizeof(S) == 0, "Need more than 0 dimensions!"); }; 

Detailed explanation

This solution, like any other using variational patterns, depends on recursion.

I wanted to express recursion on the outer loop, so I started with the base case; end of cycle. This is the case when the beginning coincides with the end:

 template<int Dim, size_t B, size_t E> struct iterate<Dim, E, B, E> { /*..*/}; 

Note that this is a specialization for <Dim, E, B, E> . The second position indicates the current index of the external circuit, and the last position indicates that the index iterates to (but not including). So, in this case, the current index coincides with the last, indicating that we have finished the cycle (and, therefore, the function "do nothing").

The recursive case for the outer loop includes a scenario in which the loop index is smaller than the index for iteration. In template conditions, the second position is less than the fourth position:

 template<int Dim, size_t S, size_t B, size_t E> struct iterate {/*...*/} 

Please note that this is NOT a specialization.

The logic of this function is that the outer loop must signal the inner loop to start execution from the beginning, and then the outer loop continues and starts the process again for the inner loops:

 iterate<Dim-1, B, B, E>::apply (func, a..., S); iterate<Dim, S+1, B, E>::apply (func, a...); 

Notice that in the first line, the second argument of the pattern is again B , which means you have to start from the beginning again. This is necessary because another recursive case on the second line increases S (increasing the index of the outer loop).

All the time, we also accumulate arguments for moving to a function:

 ::apply(func, a..., S) 

passes the on function along with the indices of higher measurement cycles, and then adds the current cycle index ( S ). a Here is a variation pattern.

Inner loop

When I say โ€œinner loop,โ€ I mean the innermost loop. This loop should simply increase until the start index reaches the end index and tries to recurs to any lower size. In our case, this is when our Dim (Dimension) parameter is 1:

 template<size_t S, size_t B, size_t E> struct iterate<1, S, B, E> {/*...*/}; 

At this point, we finally want to call our function passed along with all the arguments that we have accumulated so far (indexes of outer loops) PLUS, the index of the innermost loop:

 func(a..., B); 

And then recurse (index index)

 iterate<1, S, B+1, E>::apply(func, a...); 

The basic example here is when the innermost loop index matches the end index (AND the dimension is 1):

 template<size_t S, size_t E> struct iterate<1, S, E, E> {/*...*/}; 

Therefore, there is "do nothing"; no work should be done because the loop ends.

Finally, I turned on one last specialization to catch a user error where they didnโ€™t indicate any dimensions:

 template<size_t S, size_t B, size_t E> struct iterate<0, S, B, E> 

Uses static_assert to always fail because sizeof(size_t) not zero:

 static_assert(sizeof(S) == 0, "Need more than 0 dimensions!"); 

Conclusion

This is a meta-program template for a specific use case. Where we essentially generate N nested loops that have the same start and end indices And we want to pass these indices to a function. We could do a little more work to make it so that the iterate structure could stand on its own, without making the assumption that the start and end indices of the outer loop coincide with the inner loops.

My favorite application of this code is that we can use it to create an N-dimensional counter. For example, a binary counter for N-bits (found in a live demo).

+3
source

Since this question still seems to be getting traffic, I thought it would be nice to show how much easier it is to do this in C ++ 17. First, the full code

demonstration

 template<size_t Dimensions, class Callable> constexpr void meta_for_loop(size_t begin, size_t end, Callable&& c) { static_assert(Dimensions > 0); for(size_t i = begin; i != end; ++i) { if constexpr(Dimensions == 1) { c(i); } else { auto bind_an_argument = [i, &c](auto... args) { c(i, args...); }; meta_for_loop<Dimensions-1>(begin, end, bind_an_argument); } } } 

Explanation:

  1. If Size is 1, we just call the provided lambda with the next index in the loop
  2. Otherwise, we create a new called object from the one provided, except that we bind the loop index to one of the called arguments. Then we return to our meta for the cycle with 1 smaller dimension.

If you are familiar with functional programming at all, this is a little easier to understand, as it is a currying application.

How it works in more specific conditions:

Do you want a binary counter that goes

0 0
0 1
ten
eleven

Thus, you create a callable object that can print two integers as follows:

 auto callable = [](size_t i, size_t j) { std::cout << i << " " << j << std::endl; }; 

And since we have two columns, we have two dimensions, so D = 2.

We call our meta for the loop defined above, like this:

 meta_for_loop<2>(0, 2, callable); 

end argument of meta_for_loop is 2 instead of 1, because we model the half-closed interval [start, end), which is often found in programming, because people often want the first index to be included in their loop, and then they want to iterate (end - start) times.

Let's go through the algorithm:

  1. Dimensions == 2, so we are not mistaken in our static statement
  2. We start the iteration, i = 0
  3. Dimensions == 2, so we introduce the "else" branch of our constexpr if
    • We create a new called object that captures the bind_an_argument passed to the called bind_an_argument and bind_an_argument to reflect that we are linking one argument of the provided called object c .

So bind_an_argument effectively looks like this:

 void bind_an_argument(size_t j) { c(i, j); } 

Note that i remains the same, but j is a variable. This is useful in our meta cycle because we want to simulate the fact that the outer loop remains at the same index, while the inner loop repeats throughout the range. for instance

 for(int i = 0; i < N; ++i) { for (int j = 0; j < M; ++j) { /*...*/ } } 

when i == 0 we iterate over all the values โ€‹โ€‹of j from 0 to M , and then repeat for i == 1 , i == 2 , etc.

  1. We call meta_for_loop again, except that Dimensions now 1 instead of 2 , and our Callable now bind_an_argument instead of c
  2. Dimensions == 1 so our static_assert passes
  3. We start the for(size_t = 0; < 2; ++i) loop for(size_t = 0; < 2; ++i)
  4. Dimensions == 1 so we enter the if branch of our constexpr if
  5. We call bind_an_argument with i = 1 , which calls our callable argument from above (0, 0) , the first of which was associated with the previous meta_for_loop call. This produces a conclusion

    0 0

  6. We call bind_an_argument with i == 1 , which calls our callable argument above (0, 1) , the first argument of which was connected during our previous call to meta_for_loop . This produces a conclusion

    0 1

  7. We finish the iteration, so the stack spins to the parent calling function
  8. We returned to our meta_for_loop call with Dimensions == 2 and Callable == callable . We end our first loop iteration and then increase i to 1
  9. Since Dimensions == 2 , we again enter the else branch
  10. Repeat steps 4 through 10, except that the first argument to the callable object is associated with 1 instead of 0 . This produces a conclusion

    ten
    eleven

0
source

Source: https://habr.com/ru/post/1239515/


All Articles