0

I want to code some header file whose inclusion allows me to then write template for-loops in an accessible syntax. How do I implement this? (cf. at the bottom for my best attempt).

There follows an example and criteria of what is meant with an accessible syntax.

Example:

for_loop<int,i0,ic,iE>(i){ // for(int i=i0;i<iE;i+=ic), where i0,ic,iE are constexpr int
   foo(i);
   bar<i>();
   for_loop<int,i,1,10>(j){
      boo<i,j>();
   }
}

Criteria

As met by the example, useful criteria for an accessible syntax are:

  • the scope of each for-loop is braced
  • there is no boilerplate (like creation of a lambda functions, functor classes, index sequences, confusing artifacts like (), [], ,... that enhance readability neither of loop range nor loop body, etc.)
  • the accessibility of variables and functions from within the braced scope is identical to a classical for-loop.
  • desirable: the use of <> notation for int,i0,ic,iE as to indicate them as template arguments. // the use of (i) is because I assume we need a Macro there for the symbol i

Related questions are presented here:

  • [https://stackoverflow.com/questions/13816850/is-it-possible-to-develop-static-for-loop-in-c]
  • [https://stackoverflow.com/questions/1032602/template-ing-a-for-loop-in-c]
  • [https://stackoverflow.com/questions/42019564/how-to-iterate-over-stdindex-sequence]

These questions, however, focus on the achievement of functionality. The focus of this question instead is explicitly on the achievement of an accessible syntax.

Best Attempt

The following is my best functioning attempt yet at achieving a solution:

# define template_for_loop_begin(index) \
    [&]<std::size_t... index>(std::index_sequence<index...>) {

# define template_for_loop_end(index,N) ;}(std::make_index_sequence<N>{});

// this allows writing a template for-loop as follows:
template_for_loop_begin(i)(
  bar<i>(),...
)template_for_loop_end(i,10)

However, the resulting syntax is unsatisfactory. Issues are:

  • major: the loop body is limited to one single line
  • major: the loop body must be a single function call
  • semi: there is ,... instead of ; at the end of the line
  • semi: the indexing is in the footer of the loop
  • minor the loop brackets are () instead of {}

Example of a Normal For Loop and what Problems it poses

A real-world code example is a following customer's code, using my spline library.

for(int i=0;i<1000;++i){
  int j=i;
  spline.load(j,data[j]);
}

This works fine. Now some employees have gotten to bad ideas, like:

for(int i=0;i<500;++i){
  if(data[i]>0){ j=i+500;}else{j=i;}
  spline.load(j,data[i]);
}

My software uses automatic code transformation based on a run-time that produces source code. In order for this source code transformation to yield my intended behaviour, the index j must be a constexpr. Hence, I would like to make it template. However, I cannot educate the customer into relearning a way of writing for-loops that discourages/demotivates them from using my code. Which leads to this question.

Templates, Consteval, and Constexpr, and Type

Thanks for Yakk - Adam Nevraumont's kindly provided answer, we can use static_foreach below:

#include <utility>
template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const& {
     [&]<auto...Is>(std::integer_sequence<decltype(first), Is...>) {
       ( f( std::integral_constant<decltype(Is), first+Is*increment>{} ), ...);
     }( std::make_integer_sequence< decltype(first), (limit-first)/increment >{} );
  }
};
template<auto first, auto increment, auto limit>
constexpr static_foreach_t<first, increment, limit> static_foreach = {};

// ++++

template<typename T>
struct Cxpr{
    const T t;
    consteval Cxpr(T t):t(t){}
};

void foo(int){}
void bar(Cxpr<int>){}
template<int> void boo(void){}

int main(){
    static_foreach<3, 2, 7>->*[&](auto i) {
        foo(i);
        //bar(i);
        boo<i>();
    };
}

However, it the code throws an error error: could not convert 'i' from 'std::integral_constant<int, 3>' to 'Cxpr<int>' when uncommenting bar(i);, which is a consteval. Can this be solved? I tried various constructors and assigment overloads for Cxpr. Another game-breaker is that it does not work with float and double, as, e.g., in my spline library there are also functions that only prevent misuse if floating-point numbers can be asserted to be of constexpr/consteval type.

  • 2
    A) Why? B) Why??? C) How is this any more "accessible" than regular C++ loops, including the [range based `for`](https://en.cppreference.com/w/cpp/language/range-for) which is, honestly, an amazingly clean design for C++? – tadman Aug 22 '23 at 20:06
  • Because customers face difficulty in using spline.load(data) for many indices, which harms business. – violetvanillavendetta Aug 22 '23 at 20:09
  • 1
    "Customers"? "Business"? What concerns are you trying to address here? What is a good example of the code you're trying to simplify? – tadman Aug 22 '23 at 20:10
  • @tadman: Imagine you have build a spline software, where the user has to call spline.load(data[i]); for thousand indices i, and the user does not want to get accustomed to the way how for-loops for template i have to be written, or feels that it is just too impractical to always program a function for the loop body. – violetvanillavendetta Aug 22 '23 at 20:13
  • Sure, I can imagine, but it'd help if you showed us the kind of code you're dealing with, what users are currently doing, as we can likely suggest methods to reduce how much code is required without getting into anything as wild and exotic as what you're suggesting. For example, can you add a `load` method that takes a vector of indexes? In C++ overrides are a powerful tool that should be leveraged before going to more...desperate measures. – tadman Aug 22 '23 at 20:14
  • 1
    Please give an example of a normal C++ for loop that does what you're vaguely describing. Then explain what problems it poses. – Pete Becker Aug 22 '23 at 20:15
  • @tadman: I have exactly that spline library. The customer's employees must be able to code applications that load and evaluate splines. At the moment, i is not a template, and thus employees can use classical for-loops. But this constantly leads to misuse because for my lib to calculate the correct numbers after automatic differentiation of the employee's application, I must ensure that i is constexpr, which necessitates a way to turn said existing for-loops into template-for-loops. – violetvanillavendetta Aug 22 '23 at 20:17
  • You keep using words when we need to see *code*. – tadman Aug 22 '23 at 20:21
  • 1
    @tadman: I extended the question with the example that Pete and you asked for, if I understood correctly. Does this help? – violetvanillavendetta Aug 22 '23 at 20:26
  • That does help, but it also suggests that maybe you need a range-based loader, like `spline.load_range(0, 1000, data)` which just does all the dirty work for you. In the second case what is the intent? Loading data from different offsets? If you can use `lambda` you can make your loader pretty extensible by having a mapping function available. – tadman Aug 22 '23 at 20:32
  • @tadman: Yes, however, at generated and implemented scope, an array of contiguous names in one life-time might not be contiguous in another. Basically saying: I'd need to add significant functionality which I simply want to avoid. But of course you are right. With regards on the user's intent, I never bother to wonder :) – violetvanillavendetta Aug 22 '23 at 20:35
  • And I thought that I had stupid customers.. – Swift - Friday Pie Aug 22 '23 at 20:52

1 Answers1

2
for_loop<int,i0,ic,iE>(i){// for(int i=i0;i<iE;i+=ic), where i0,ic,iE are constexpr int

this is an easy syntax:

for( int i:range<i0,ic,iE> ) {
  // body
}

but I suspect this fails due to constraints you have chosen not to mention (like, wanting to use i in a constexpr context).

To pull that off you are going to need a lambda hiding from the user.

a macro-free version could look like:

foreach<first, increment, limit>->*[&](auto i) {
};

macro injection of the name i requires that the macro be expanded there.

#define FOREACH( FIRST, INCREMENT, LIMIT, NAME ) \
  foreach FIRST, INCREMENT, LIMIT ->* [&](auto NAME)

where users do

// Note the <> below:
FOREACH( <i0, ic, iE>, i ) {
  // function body
}; //note the ; requirement

and it expands as the above. I find this to be a bad idea, because if it fails to compile you'll get a pile of nonsense error messages the user has no idea why it happens, possibly involving preprocessor problems and possibly in the generated code.

The ->* trick is that foreach<first, increment, limit> is a template value with an overloaded ->* operator that takes a callable on the right hand side.

template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const&& {
     [&]<auto...Is>(std::index_sequence<Is...>) {
       ( f( std::integral_constant<decltype(Is), first+Is*increment>{} ), ...);
     }( std::make_integer_sequence< decltype(first), (limit-first)/increment >{} );
  }
};

template<auto first, auto increment, auto limit>
constexpr static_foreach_t static_foreach = {};

so, ignoring the requirements of yours I do not like:

static_foreach<1, 5, 11>->*[&](auto i) {
  int array[i] = {0};
};

or, with the macro (bad idea):

#define STATIC_FOREACH(FIRST,INCREMENT,LIMIT,NAME) \
  static_foreach FIRST, INCREMENT, LIMIT ->*[&](auto NAME)

STATIC_FOREACH( <1, 5, 11>, i) {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n;
};

or in my preferred version:

static_foreach<1, 5, 11>->*[&](auto i) {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n";
};

while ->*[&](auto i) might be a bunch of magic tokens, hiding said magic tokens behind a macro does not make things friendlier in my experience.

The end user won't understand the macro nor the magic tokens, but the macro means that the end user when the macro produces an error will have no hope of figuring out what went wrong. Meanwhile, if the magic tokens produce an error, you get the actual compiler helping out a bit.

Live example.

Oh, and one really huge gotcha is the return problem. A return from the body of the loop won't do what the user expects.

Having the function call and lambda be visible makes this a bit less surprising.

Eliminating this weakness isn't possible, as you cannot make a function you call in C++ get control over the return flow of the code calling it, barring some seriously strange coroutine magic cases.


Now the type of i isn't a constexpr int. We can fix this with a bit more tomfoolery:

template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const& {
     [&]<auto...Is>(std::integer_sequence<std::size_t, Is...>) {
       ( f.template operator()<first+Is*increment>(), ...);
     }( std::make_integer_sequence<std::size_t, (limit-first)/increment >{} );
  }
};

template<auto first, auto increment, auto limit>
constexpr static_foreach_t<first, increment, limit> static_foreach = {};

#define STATIC_FOREACH(FIRST,INCREMENT,LIMIT,NAME) \
  static_foreach FIRST, INCREMENT, LIMIT ->*[&]<auto NAME>()

here instead of passing a std::integral_constant we pass the value as a template argument to operator().

Use changes slightly in the non-macro case:

static_foreach<1, 5, 11>->*[&]<auto i>() {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n";
};

as we take i as a template non-type parameter. (auto i could read int i or whatever).

For the macro case it looks identical:

STATIC_FOREACH( <1, 5, 11>, i ) {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n";
};

Support for non-integral values for the loop requires a small tweak:

template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const& {
     [&]<auto...Is>(std::integer_sequence<std::size_t, Is...>) {
       ( f.template operator()<first + Is*increment>(), ...);
     }( std::make_integer_sequence< std::size_t, static_cast<std::size_t>((limit-first)/increment) >{} );
  }
};

which experimentally works for floats.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Thank you very much. For next time, how could I have made the requirement on ``i`` clearer? I tried to make clear by term "template" and in the example with ``bar();``. – violetvanillavendetta Aug 22 '23 at 20:32
  • @YakkAdamNevraumont: I have an semi issue when using your solution in conjunction with a consteval wrapper, in that std::integral_constant cannot be converted into constexpr int, or consteval int (actual, Cxpr, cf. attendum to question). Can this be mitigated? – violetvanillavendetta Aug 22 '23 at 21:18
  • @violetvanillavendetta That is just the double user conversion problem. Cast to `int` like `bar((int)i)`, or do `bar(i())`. I could jump through hoops and make `i` be a true template constant; `[&](std::integral_constant)` sort of thing. Operator `->*` could even use `template operator()` instead. – Yakk - Adam Nevraumont Aug 23 '23 at 13:26