Macros for common loops?

I'm working with a lot of LEDs but this applies to code in general. If something like this is possible I want to know for further usage down the road. Specifically for more complicated, but similar, situations.

One thing I use a TON basically looks like the following:

for(int strip = 0; strip < STRIPS; strip++)
{
  for(int led = 0; led < LEDS; led++)
  {
    //Do Stuff
    LEDCollection[strip][led] = Color(255,255,255)
  }
}

Is there a possible way to define a macro or function template that could simplify this common setup? Ultimately the goal might look something like this:

forLEDS(strip, led)
{
  LEDCollection[strip][led] = Color(255,255,255);
}

One way, which I have used in prior projects, is the following:

//Definition
#define forLEDS for(int strip = 0; strip < STRIPS; strip++){ \
                  for(int led = 0; led < LEDS; led++)

//Implementation
forLEDS
{
  LEDCollection[strip][led] = Color(255, 255, 255);
}}

Certainly much cleaner especially on the implementation. But it isn't obvious that you have to remember to use two closing curly brackets. And you don't get to define your variable names on the spot. The latter issue being fixable using #define forLEDS(_a, _b) and then changing the implementation accordingly. But this still wouldn't be ideal for more complex cases.

I figure another way that might be possible would be via some weird combination of a pointer to a function which passes by reference.

//Definition
void forLEDS(void(*dostuff(&int, &int))
{
  for(int s = 0; s < STRIPS; s++)
  {
    for(int l = 0; l < LEDS; l++)
    {
      dostuff(s,l);
    }
  }
}

//Implementation
int strip, led;
forLEDS([](strip, led){
  LEDCollection[strip][led] = Color(255,255,255);
});

Ignoring the fact that that it's likely wrong, it is clearly more of a mess to implement.

Got any other ideas? I figure it might involve templates or something else in C++ I'm not familiar with.

I tried to pick out your general case which looks like setting all elements of a given 2 dimensional array to a defined value.
Here is a macro which appears to do this with a rough test:

#include <iostream>

using namespace std;

#define COLLECTION_SET( array, s1, s2, set )  do { \
  for ( int i = 0 ; i < s1 ; i ++ ) {       \
      for ( int j = 0 ; j < s2 ; j ++ )     \
      array[i][j] = (set) ;                 \
  }                                         \
} while ( 0 )

int collection[5][3] ; 

int main()
{
   COLLECTION_SET(  collection, 5, 3, 7+3 ) ;
   
   cout << collection[4][2] << endl; 
   
   return 0;
}

Disclaimer. I'm not macro expert (never use them myself), but this appears to be a good guide: Tutorials - C Preprocessor Tricks - Cprogramming.com

if there are a collection of operations you need to perform indexed by strip and led, you could pass a function pointer to a loop function

The problem with this kind of macros is that they hide the structure of the program. A programmer reading your code has to go look at the definition of the macro to make sure all braces match, has to check the names of the loop variables, and so on.
In this case, I don't think this extra cognitive load is worth it, just to save some typing on the for loops. In cases where you need to duplicate a large block of code or declarations, you could consider a macro like this (if it can't be done using a function), but I don't think a for loop qualifies.

I think this

for (auto &strip : LEDCollection)
  for (auto &led : strip)
    led = Color(255, 255, 255);

is easier to read than

forLEDS
{
  LEDCollection[strip][led] = Color(255, 255, 255);
}}

because it's immediately clear what's going on, without magic variables strip and led that appear out of nowhere.

The macro-free functional approach you proposed has some syntax errors, you could correct it as follows:


void forLEDS(void (*dostuff)(size_t strip_idx, size_t led_idx)) {
  for(size_t s = 0; s < STRIPS; ++s)
    for(size_t l = 0; l < LEDS; ++l)
      dostuff(s, l);
}

forLEDS([](size_t strip, size_t led) {
  LEDCollection[strip][led] = Color(255,255,255);
});

I think it's better than the macro approach, but there's still implicit coupling between the forLEDS function and the lambda function you pass to it: the forLEDS function uses the STRIPS and LEDS constants, and the lambda uses the LEDCollection array (?). If for whatever reason the sizes no longer match the array you're using in your lambda function, it is very hard to see that from looking at the code because they are separated by a function call.

Specifying array sizes/indices manually is always risky. (Which is why I would recommend the range-based for loops in the first snippet of this post.)
Depending on the type of LEDCollection, you could consider a function like std::ranges::for_each:

template <class DoubleIterable, class Action>
void for_each2D(DoubleIterable &iterable, Action action) {
  for (auto &row : iterable)
    for (auto &element : row)
      action(element);
}

for_each2D(LEDCollection, [](Color &c) {
    c = Color(255,255,255);
});

Now you're no longer specifying the “invisible” array dimensions manually, the compiler figures them out for you. If you wanted to, you could add extra arguments to your lambda functions to get the strip and LED indices.

regarding that:

you should look into the examples of your library, if there is some single liner like

strip.fill( Color(255,255,255));

And if it isn’t immediately clear because I’ve not seen that auto thing before, or am only vaguely aware of it, at least when I look into it, it is part of the language being used, not a one-off invention… and maybe I expand my knowledge applicable in all circumstances just a bit.

Probably gonna write loops out long hand still for awhile. After having done in excess of 2^14 times, in comes out of my elbows fast enough. :wink:

a7

If you want to use macros in your own code as a typing shortcut, why not? But if anyone else is going to look at your code - a colleague perhaps or here on the forum, you'll confuse them at best or just have your help request ignored by more folks.

Also, I find that most of my coding time is spent thinking, designing, testing and debugging. Very little of it is actual typing, so the saving is perhaps less than you would expect.

1 Like

You can read for (auto &strip : LEDCollection) as “for strip in LEDCollection”, similar to writing for strip in LEDCollection in Python.

The auto keyword simply tells the compiler to deduce the type, similar to var or let in some other languages. I don't know what the exact type of LEDCollection is in this example, but assuming it's a 2D array of type Color (i.e. Color LEDCollection[STRIPS][LEDS];), without auto, you would have to write something like this:

  for (Color (&strip)[LEDS] : LEDCollection)
    for (Color &led : strip)
      led = Color(255, 255, 255);

Spelling out the types in this situation doesn't really add anything IMO, if anything, it makes it significantly harder to read, so I just let the compiler figure it out for me by using auto.

The main advantage of range-based for loops is not typing speed, but the fact that the compiler figures out the size for you. If you specify the size manually, there's always a chance you get it wrong, and you often need a separate constant just to store the size of an array (even though the compiler already knows the size), or use sizeof(array) / sizeof(*array).
Range-based for loops don't just work on arrays, you can use them on anything that's iterable.

#define forLEDS(stripIndex, ledIndex) \
for(int stripIndex = 0; stripIndex < STRIPS; stripIndex++) \
  for(int ledIndex = 0; ledIndex < LEDS; ledIndex++)
 
  forLEDS(strip, led)
  {
    //Do Stuff
    LEDCollection[strip][led] = Color(255,255,255);
  }