Search code examples
cfor-loopstructunionsreadability

Using struct field as loop counter?


Some background to the issue

if I have a struct like

typedef struct {
    idx_type type;
    union {
        char *str;
        int num;
    } val
} cust_idx;

and I have loops like this

for (i = 0; i < some_get(x); i++) {
    some_fun(z, NULL, i);
}

that I want to refactor to use the struct like some_fun(z, idx) where idx is one of my cust_idx structs, would it be best to keep i as the loop counter and update idx or change the for header to use idx.val.num instead of i?

For the purposes of this, assume idx_type is an enum for string and number types, and all other fields will have macros, but I'm only going to use the IDX_NUM macro here as I'm not worried about anything to do with idx.type.

To sum up my concerns:

  1. Will it be readable? I don't want to leave behind a mess that someone will read and just shake their head...
  2. Is it advised against?
  3. Which of these is the best solution?

Struct field as loop counter

#define IDX_NUM(x) (x.val.num)
...
cust_idx j;
j.type = TYPE_num;
for (IDX_NUM(j) = 0; IDX_NUM(j) < some_get(x); IDX_NUM(j)++) {
    some_fun(z, j);
}

This does the same as the original, but the using struct field/macro extends and complicates the for loop header in my opinion but it's still fairly understandable.

Modify struct with original counter

cust_idx j;
j.type = TYPE_num;
for (i = 0; i < some_get(x); i++) {
    IDX_NUM(j) = i;
    some_fun(z, j);
} 

This results in the least changes from old code logically, but will end in by far the largest amount of code due to the add assignment lines.

Pointer to struct field

cust_idx j;
int *i = &(j.val.num);
j.type = TYPE_num;
for ((*i) = 0; (*i) < some_get(x); (*i)++) {
    some_fun(z, j);
} 

I'm not sure how good this would be in the long run, or if it's advised against.


Solution

  • As to readability, I would always prefer separate loop counters.

    EDIT: The following in italic is not right in this specific case as C structs by default are passed as value copies over the stack, so passing j to some_fun() in the loop is ok. But I'll leave the caveat here, as it applies to many similar situations, where the struct or array is passed by a pointer value. (aka 'passed by reference').

    That is especially true in code like you posted, where you call a function with the structure as an argument inside the loop. If I don't know what some_fun() does, I can only hope that the struct's member that I use as a loop counter is not modified. And hope is not a strategy.

    So, unless there are very hard reasons for doing otherwise, I'd always place readability first. Remember: If you write code that is at the limits of your own syntactic and semantic capabilities, you will have very little fun debugging such code, as debugging is an order of magnitude harder than writing (buggy) code. ;)

    Addition: You could look at the disassemblies of all variants. The compiler might do a lot of optimizations here, especially if it can 'see' some_fun().