Is it legal to index into a struct?

Solution 1:

It is illegal 1. That's an Undefined behavior in C++.

You are taking the members in an array fashion, but here is what the C++ standard says (emphasis mine):

[dcl.array/1]: ...An object of array type contains a contiguously allocated non-empty set of N subobjects of type T...

But, for members, there's no such contiguous requirement:

[class.mem/17]: ...;Implementation alignment requirements might cause two adjacent members not to be allocated immediately after each other...

While the above two quotes should be enough to hint why indexing into a struct as you did isn't a defined behavior by the C++ standard, let's pick one example: look at the expression (&thing.a)[2] - Regarding the subscript operator:

[expr.post//expr.sub/1]: A postfix expression followed by an expression in square brackets is a postfix expression. One of the expressions shall be a glvalue of type “array of T” or a prvalue of type “pointer to T” and the other shall be a prvalue of unscoped enumeration or integral type. The result is of type “T”. The type “T” shall be a completely-defined object type.66 The expression E1[E2] is identical (by definition) to ((E1)+(E2))

Digging into the bold text of the above quote: regarding adding an integral type to a pointer type (note the emphasis here)..

[expr.add/4]: When an expression that has integral type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the expression P points to element x[i] of an array object x with n elements, the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i + j] if 0 ≤ i + j ≤ n; otherwise, the behavior is undefined. ...

Note the array requirement for the if clause; else the otherwise in the above quote. The expression (&thing.a)[2] obviously doesn't qualify for the if clause; Hence, Undefined Behavior.


On a side note: Though I have extensively experimented the code and its variations on various compilers and they don't introduce any padding here, (it works); from a maintenance view, the code is extremely fragile. you should still assert that the implementation allocated the members contiguously before doing this. And stay in-bounds :-). But its still Undefined behavior....

Some viable workarounds (with defined behavior) have been provided by other answers.



As rightly pointed out in the comments, [basic.lval/8], which was in my previous edit doesn't apply. Thanks @2501 and @M.M.

1: See @Barry's answer to this question for the only one legal case where you can access thing.a member of the struct via this parttern.

Solution 2:

No. In C, this is undefined behavior even if there is no padding.

The thing that causes undefined behavior is out-of-bounds access1. When you have a scalar (members a,b,c in the struct) and try to use it as an array2 to access the next hypothetical element, you cause undefined behavior, even if there happens to be another object of the same type at that address.

However you may use the address of the struct object and calculate the offset into a specific member:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

This has to be done for each member individually, but can be put into a function that resembles an array access.


1 (Quoted from: ISO/IEC 9899:201x 6.5.6 Additive operators 8)
If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.

2 (Quoted from: ISO/IEC 9899:201x 6.5.6 Additive operators 7)
For the purposes of these operators, a pointer to an object that is not an element of an array behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type.

Solution 3:

In C++ if you really need it - create operator[]:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

it is not only guaranteed to work but usage is simpler, you do not need to write unreadable expression (&thing.a)[0]

Note: this answer is given in assumption that you already have a structure with fields, and you need to add access via index. If speed is an issue and you can change the structure this could be more effective:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

This solution would change size of structure so you can use methods as well:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

Solution 4:

For c++: If you need to access a member without knowing its name, you can use a pointer to member variable.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;