Access struct members as if they are a single arra

2019-01-09 13:33发布

问题:

I have two structures, with values that should compute a pondered average, like this simplified version:

typedef struct
{
  int v_move, v_read, v_suck, v_flush, v_nop, v_call;
} values;

typedef struct
{
  int qtt_move, qtt_read, qtt_suck, qtd_flush, qtd_nop, qtt_call;
} quantities;

And then I use them to calculate:

average = v_move*qtt_move + v_read*qtt_read + v_suck*qtt_suck + v_flush*qtd_flush + v_nop*qtd_nop + v_call*qtt_call;

Every now and them I need to include another variable. Now, for instance, I need to include v_clean and qtt_clean. I can't change the structures to arrays:

typedef struct
{
    int v[6];
} values;
typedef struct
{
    int qtt[6];
} quantities;

That would simplify a lot my work, but they are part of an API that need the variable names to be clear.

So, I'm looking for a way to access the members of that structures, maybe using sizeof(), so I can treat them as an array, but still keep the API unchangeable. It is guaranteed that all values are int, but I can't guarantee the size of an int.

Writing the question came to my mind... Can a union do the job? Is there another clever way to automatize the task of adding another member?

Thanks, Beco

回答1:

If all members a guaranteed to be of type int you can use a pointer to int and increment it:

int *value = &(values.v_move);
int *quantity = &(quantities.qtt_move);
int i;
average = 0;
// although it should work, a good practice many times IMHO is to add a null as the last member in struct and change the condition to quantity[i] != null.
for (i = 0; i < sizeof(quantities) / sizeof(*quantity); i++)
    average += values[i] * quantity[i];

(Since the order of members in a struct is guaranteed to be as declared)



回答2:

What you are trying to do is not possible to do in any elegant way. It is not possible to reliably access consecutive struct members as an array. The currently accepted answer is a hack, not a solution.

The proper solution would be to switch to an array, regardless of how much work it is going to require. If you use enum constants for array indexing (as @digEmAll suggested in his now-deleted answer), the names and the code will be as clear as what you have now.

If you still don't want to or can't switch to an array, the only more-or-less acceptable way to do what you are trying to do is to create an "index-array" or "map-array" (see below). C++ has a dedicated language feature that helps one to implement it elegantly - pointers-to-members. In C you are forced to emulate that C++ feature using offsetof macro

static const size_t values_offsets[] = { 
  offsetof(values, v_move),
  offsetof(values, v_read),
  offsetof(values, v_suck),
  /* and so on */
};

static const size_t quantities_offsets[] = { 
  offsetof(quantities, qtt_move),
  offsetof(quantities, qtt_read),
  offsetof(quantities, qtt_suck),
  /* and so on */
};

And if now you are given

values v;
quantities q;

and index

int i;

you can generate the pointers to individual fields as

int *pvalue = (int *) ((char *) &v + values_offsets[i]);
int *pquantity = (int *) ((char *) &q + quantities_offsets[i]);

*pvalue += *pquantity;

Of course, you can now iterate over i in any way you want. This is also far from being elegant, but at least it bears some degree of reliability and validity, as opposed to any ugly hack. The whole thing can be made to look more elegantly by wrapping the repetitive pieces into appropriately named functions/macros.



回答3:

Writing the question came to my mind... Can a union do the job? Is there another clever way to automatize the task of adding another member?

Yes, a union can certainly do the job:

union
{
  values v;    /* As defined by OP */
  int array[6];
} u;

You can use a pointer to u.values in your API, and work with u.array in your code.

Personally, I think that all the other answers break the rule of least surprise. When I see a plain struct definition, I assume that the structure will be access using normal access methods. With a union, it's clear that the application will access it in special ways, which prompts me to pay extra attention to the code.



回答4:

It really sounds as if this should have been an array since the beggining, with accessor methods or macros enabling you to still use pretty names like move, read, etc. However, as you mentioned, this isn't feasible due to API breakage.

The two solutions that come to my mind are:

  • Use a compiler specific directive to ensure that your struct is packed (and thus, that casting it to an array is safe)
  • Evil macro black magic.


回答5:

this problem is common, and has been solved in many ways in the past. None of them is completely safe or clean. It depends on your particuar application. Here's a list of possible solutions:

1) You can redefine your structures so fields become array elements, and use macros to map each particular element as if it was a structure field. E.g:

struct values { varray[6]; };
#define v_read varray[1]

The disadvantage of this approach is that most debuggers don't understand macros. Another problem is that in theory a compiler could choose a different alignment for the original structure and the redefined one, so the binary compatibility is not guaranted.

2) Count on the compiler's behaviour and treat all the fields as it they were array fields (oops, while I was writing this, someone else wrote the same - +1 for him)

3) create a static array of element offsets (initialized at startup) and use them to "map" the elements. It's quite tricky, and not so fast, but has the advantage that it's independent of the actual disposition of the field in the structure. Example (incomplete, just for clarification):

int positions[10];
position[0] = ((char *)(&((values*)NULL)->v_move)-(char *)NULL);
position[1] = ((char *)(&((values*)NULL)->v_read)-(char *)NULL);
//...
values *v = ...;
int vread;
vread = *(int *)(((char *)v)+position[1]);

Ok, not at all simple. Macros like "offsetof" may help in this case.



回答6:

How about using __attribute__((packed)) if you are using gcc?

So you could declare your structures as:

typedef struct
{
    int v_move, v_read, v_suck, v_flush, v_nop, v_call;
} __attribute__((packed)) values;

typedef struct 
{
    int qtt_move, qtt_read, qtt_suck, qtd_flush, qtd_nop, qtt_call;
} __attribute__((packed)) quantities;

According to the gcc manual, your structures will then use the minimum amount of memory possible for storing the structure, omitting any padding that might have normally been there. The only issue would then be to determine the sizeof(int) on your platform which could be done through either some compiler macros or using <stdint.h>.

One more thing is that there will be a performance penalty for unpacking and re-packing the structure when it needs to be accessed and then stored back into memory. But at least you can be assured then that the layout is consistent, and it could be accessed like an array using a cast to a pointer type like you were wanting (i.e., you won't have to worry about padding messing up the pointer offsets).

Thanks,

Jason