107

Regardless of how 'bad' the code is, and assuming that alignment etc are not an issue on the compiler/platform, is this undefined or broken behavior?

If I have a struct like this :-

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

struct data thing;

Is it legal to access a, b and c as (&thing.a)[0], (&thing.a)[1], and (&thing.a)[2]?

In every case, on every compiler and platform I tried it on, with every setting I tried it 'worked'. I'm just worried that the compiler might not realize that b and thing[1] are the same thing and stores to 'b' might be put in a register and thing[1] reads the wrong value from memory (for example). In every case I tried it did the right thing though. (I realize of course that doesn't prove much)

This is not my code; it's code I have to work with, I'm interested in whether this is bad code or broken code as the different affects my priorities for changing it a great deal :)

Tagged C and C++ . I'm mostly interested in C++ but also C if it is different, just for interest.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
jcoder
  • 29,554
  • 19
  • 87
  • 130
  • 51
    No, it is not "legal". It is undefined behavior. – Sam Varshavchik Nov 14 '16 at 13:47
  • 11
    It works for you in this very simple case because the compiler doesn't add any padding between the members. Try with structures using differently sized types and will come crashing down. – Some programmer dude Nov 14 '16 at 13:50
  • 7
    Digging up the past - UB used to be nick-named [nasal daemons](http://catb.org/jargon/html/N/nasal-demons.html). – Adrian Colomitchi Nov 14 '16 at 13:51
  • I'm aware of padding of course. I think I'd better change this code first then :) – jcoder Nov 14 '16 at 13:54
  • 21
    Well great, here I stumble in because I follow the C tag, read the question, then write an answer which only applies to C, because I didn't see the C++ tag. C and C++ are very different here! C allows type punning with unions, C++ does not. – Lundin Nov 14 '16 at 14:04
  • It breaks the strict aliasing rule in C. – Klas Lindbäck Nov 14 '16 at 14:08
  • This is called "trick" or "hack" and I cannot recommend to do it. – i486 Nov 14 '16 at 14:08
  • 7
    If you need to access the elements as an array, define them as an array. If they need to have different names, use the names. Trying to have your cake and eat it will lead to indigestion eventually — probably at the most inconvenient imaginable time. (I think the index 0 is legal in C; the index 1 or 2 is not. There are contexts in which a single element is treated as an array of size 1.) – Jonathan Leffler Nov 14 '16 at 14:12
  • @Lundin Thank you for your answer. Although my code was in C++ I was also interested in how C worked too so your answer was welcome to me – jcoder Nov 14 '16 at 14:22
  • 1
    @jcoder This is actually one of the major reasons why C++ is considered bad for embedded systems. – Lundin Nov 14 '16 at 14:24
  • 3
    @Lundin Do you mean people assuming that it is slow? I think there are a lot of assumptions which are not valid anymore. There are valid reasons, e.g. increased runtime size, some performance-overhead from virtual dispatch, but C++ offers a lot that embedded systems would benefit from. – Jens Nov 14 '16 at 15:57
  • Wait a minute! The first one works because of the common prefix rule. int x; has the same prefix as struct { int a,b,c; } Two types with the same prefix can always be punned, but sometimes you have to force the compiler to recognize you did so (write to a volatile pointer or call an external function). – Joshua Nov 14 '16 at 17:28
  • 1
    @Jens No, I mean what I wrote: C++ does not allow type punning, it is undefined behavior. C++ is therefore unsuitable for tasks such as declaring hardware registers, data serialization, data protocols, data communication, where you would use unions. C++ being slow has nothing to do with that. – Lundin Nov 15 '16 at 07:28
  • ITT: everyone saying "it's undefined behaviour" with no justification – M.M Nov 15 '16 at 08:05
  • 2
    @Lundin That's a very strong assumption because there are alternatives to type-punning, e.g. using the address casted to a pointer. Additionally, C++ is used for a lot of the tasks you deem it unsuitable, so many people seem to disagree. Especially modern C++ is well suited for these tasks: https://accu.org/index.php/journals/281, http://blog.xpcc.io/2015/02/25/typesafe-register-access-in-c++/, http://www.embedded.com/design/programming-languages-and-tools/4438660/Modern-C--in-embedded-systems---Part-1--Myth-and-Reality. – Jens Nov 15 '16 at 08:38
  • 3
    @Jens There's no point in trying to convince me, I've gone from enthusiastic C++ fan to loathing C++, over the past 15 years. This comes from using the C++ language and C++ tools in real world systems. Add to that, that the C++ committee have now gone "all in" on meta programming with C++11 and C++whatever. I see no hope at all for C++. Lets just hope something better will eventually come along. Until then, we are stuck with C. – Lundin Nov 15 '16 at 09:02
  • 3
    @Lundin I will never understand why anybody picks C over C++ if there is not technical necessity. Even if you do not do OO, the better type-safety , expressiveness, abstraction and encapsulation facilities are a huge win. I have not used `void*` or macros in real-world projects for years, while I see these all the time in C code. Also many C-idioms model things available in C++, e.g. encapsulation by classes by using many functions with prefixes and void* objects. All not type-safe. But anyway, if you are happy with C then use it. I just don't agree that its superior for any project. – Jens Nov 15 '16 at 11:35
  • It's one of those evil techniques that usually work just fine. Yes, it is undefined behavior, however, the greatest danger to this code is that the optimizer proves the UB and proceeds to cripple the code. Compiled with `-O0`, the code will work fine on pretty much every system out there. – cmaster - reinstate monica Nov 15 '16 at 16:46
  • 3
    @Jens The plus of using C over C++ is, that you don't have to argue with people who absolutely want to use template meta-programming or use exceptions. If you want to keep a C++ project clean of those, you are always on the defense. In a C project, it's just a non-topic. – cmaster - reinstate monica Nov 15 '16 at 16:49
  • Possible duplicate of [Accessing consecutive members with a single pointer](http://stackoverflow.com/questions/24825646/accessing-consecutive-members-with-a-single-pointer) – Nikos Athanasiou Nov 28 '16 at 11:10

10 Answers10

74

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.

WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
  • Thanks :) I'll prioritize changing this code then :) – jcoder Nov 14 '16 at 13:53
  • Does anyone know where is the standard this is stated? (Or where it would say something if it was legal?) Just for interest – jcoder Nov 14 '16 at 13:55
  • 1
    @jcoder It is defined in [class.mem](http://eel.is/c++draft/class.mem). See the last paragraph for the actual text. – NathanOliver Nov 14 '16 at 13:57
  • @jcoder Pointer arithmetic on non-arrays, for example. See http://eel.is/c++draft/expr.add – dyp Nov 14 '16 at 13:58
  • No idea what that link is, but C++11 (draft n3376) defines "strict aliasing" in 3.10/10. Is this link a later or earlier version than that? – Lundin Nov 14 '16 at 14:16
  • 4
    Strict alising isn't relevant here. The type int is contained within the aggregate type and this type may alias int. `- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),` – 2501 Nov 14 '16 at 14:37
  • 1
    @The downvoters, care to comment? -- and to improve or point out where this answer is wrong? – WhiZTiM Nov 14 '16 at 14:47
  • 4
    Strict aliasing is irrelevant to this. The padding is not part of the stored value of an object. Also this answer fails to address the most common case: what happens when there is no padding. Would recommend deleting this answer actually. – M.M Nov 15 '16 at 08:01
  • So the standard says structs can have padding "*as necessary to achieve appropriate alignment.*". Does it allow the possibility of padding for other reasons? If not, consecutive elements of the same type should always be packed exactly as tightly as they would be in an array. IDK if types with an alignment requirement higher than their size are possible, but if so, an array of them would have the same padding between elements, right? – Peter Cordes Nov 15 '16 at 08:31
  • @M.M, I haven't been able to produce a case where there is padding, without explicitly doing some deliberate sorcery. Well, I have completely overhauled the answer, would you still recommend deleting it? – WhiZTiM Nov 15 '16 at 10:18
  • @PeterCordes, I do not know of any other reason(s) to introduce "gaps" or padding other than alignment reasons... – WhiZTiM Nov 15 '16 at 10:33
  • I'd recommend removing the strict aliasing quote since your answer doesn't refer to it anymore – M.M Nov 15 '16 at 11:23
  • The last quotation from [basic.lval/8], concerning aliasing issues, does not apply; the type is proper -- a pointer to int; it's just that it points outside the original object. – Peter - Reinstate Monica Nov 15 '16 at 14:59
  • Padding will usually occur as soon as you change the member type to `char`. Or with the advent of 128 bit CPUs, with compilers which leave `int` 32 or 64 bit. – Peter - Reinstate Monica Nov 15 '16 at 15:02
  • 1
    Done! I've removed the paragraph about strict-aliasing. – WhiZTiM Nov 15 '16 at 15:41
  • Why does it "obviously" not qualify for the if? – Barry Nov 16 '16 at 14:56
  • To clarify, `&thing.a + 1` is perfectly well-defined based on expr.add – Barry Nov 16 '16 at 15:04
  • @Barry, the *if* clause is only satisfied when `&thing.a` points to an array. Note: "*... **if** the expression `P` points to element `x[i]` **of an array** object `x` with `n` elements...*" . Here, `P` represents `&thing.a` – WhiZTiM Nov 16 '16 at 15:10
  • 1
    @WhiZTiM It's considered an array of one element. – Barry Nov 16 '16 at 15:18
  • @Barry, ...lol. Thats funny, but [I agree](http://eel.is/c++draft/expr.add#footnote-86). :-)... Still, in that case, the only legal thing would be `(&thing.a)[0]`, otherwise, we still don't satisfy the requirements of the *if* clause, because we would be going out of bounds with anything other than indexing at `0`. – WhiZTiM Nov 16 '16 at 15:25
  • @WhiZTiM The upper bound is inclusive, `&thing.a + 1` is ok. You can see my answer for the normative wording. – Barry Nov 16 '16 at 15:43
48

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.

2501
  • 25,460
  • 4
  • 47
  • 87
  • 3
    Do note this only works if the class is a standard layout type. If not it is still UB. – NathanOliver Nov 14 '16 at 13:52
  • @NathanOliver I should mention that my answer only applies to C. Edited. This is one of the problems of such dual tag language questions. – 2501 Nov 14 '16 at 13:53
  • Thanks, and that's why I asked separately for C++ and C as it's interesting to klnow the differences – jcoder Nov 14 '16 at 13:54
  • @NathanOliver The address of the first member is guaranteed to coincide with the address of the C++ class if it's standard layout. However, that neither guarantees that the access is well-defined nor implies that such accesses on other classes are undefined. – Potatoswatter Nov 14 '16 at 14:22
  • would you say that `char* p = ( char* )&thing.a + offsetof( thing , b );` leads to undefined behaviour? – M.M Nov 15 '16 at 08:11
  • @M.M I think that it would. This is tricky of course since the first member shares the address with the struct and the pointer is converted to char. Do you happen to know or do you have a link to any (committee) discussion about this problem? – 2501 Nov 15 '16 at 08:18
  • @M.M If the whole struct object may be considered as an array object of type char, then no. – 2501 Nov 15 '16 at 08:32
  • @2501 Don't know of any committee discussion. I have seen plenty of discussion on places like SO and c.l.c with no resolution. Your suggestion is self-consistent; however , IMHO, neither standard explicitly says this, and it's not clear whether it would be in line with programmers' expectations. R.. (a high rep user) has argued that, in C, any pointer should be allowed to roam within the entire allocation it points into . [N2090](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2090.htm) seems to support that view although it could be clearer. – M.M Nov 15 '16 at 08:35
45

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]; }
};
Slava
  • 43,454
  • 1
  • 47
  • 90
  • Nice, I'll probably use this :) – jcoder Nov 14 '16 at 14:15
  • 1
    I'd love to see the disassembly of this, versus the disassembly of a C program using type punning. But, but... C++ is as fast as C... right? Right? – Lundin Nov 14 '16 at 14:27
  • 6
    @Lundin if you care about speed of this construction then data should be organized as an array in the first place, not as separate fields. – Slava Nov 14 '16 at 14:44
  • 2
    @Lundin in both you mean unreadable and Undefined Behavior? No thanks. – Slava Nov 14 '16 at 14:50
  • No I mean both as an array and as in individual names, at once. Readable, fast and well-defined (in C). `typedef union { struct { int a; int b; int c; }; int array[3]; } type_t;`. Though you must static_assert that there is no padding. – Lundin Nov 14 '16 at 15:16
  • And that will give `thing.a` or `thing.array[0]`. Most notably, it won't spew a load of extra instructions into the binary and cause slower execution just for the sake of using operator overloading. – Lundin Nov 14 '16 at 15:24
  • 1
    @Lundin Operator overloading is a compile-time syntactic feature that does not induce any overhead compared to normal functions. Take a look at https://godbolt.org/g/vqhREz to see what the compiler actually does when it compiles the C++ and C code. It's amazing what they do and what one expects them to do. I personally prefer better type-safety and expressiveness of C++ over C a million times. And it works all the time without relying on assumptions about padding. – Jens Nov 14 '16 at 15:52
  • I would think it would be better to have members a, b, and c yield references to locations 0, 1, and 2 of the array. Compilers are likely to generate inefficient code for the [] operator, but good code for member access implemented as described. Further, having an array as a real backing type would make it compatible with methods expecting an `int*`. – supercat Nov 14 '16 at 16:09
  • 1
    @Jens: It's not surprising that it all optimizes away with a compile-time constant index. But if you have that, you could have just use a named member. Where gcc and clang fall on their faces with the `switch` is with a runtime-variable index. They actually branch on the index, but of course the C equivalent is just a simple sign-extend and index. https://godbolt.org/g/TAujru – Peter Cordes Nov 14 '16 at 16:56
  • 1
    @Lundin: I wondered, too. Union-based type punning makes perfect asm, even for runtime-variable indices, like you'd expect. The switch makes garbage, and only optimizes away with constant indices. See the godbolt link in my prev comment. – Peter Cordes Nov 14 '16 at 16:57
  • 1
    You would get much better performance from making the actual storage an array, and making `a`, `b`, and `c` into member functions that return references to array elements. https://godbolt.org/g/CU7apU. @Lundin: you might be interested in how to do it without sucking in C++. – Peter Cordes Nov 14 '16 at 17:05
  • @Lundin I provided solution where you can access through `thing.a` and `thing.array[0]` and you do not need to worry about padding. – Slava Nov 14 '16 at 17:09
  • 2
    Those references will double the size of the thing at least. Just do `thing.a()`. – T.C. Nov 14 '16 at 17:21
  • The one with extra reference members is nasty: given a pointer to the whole struct, the compiler can't prove that its `int &c` member is a reference to `array[2]`, so it actually loads the pointer from memory (https://godbolt.org/g/Mt5DeX): `rdi->c = esi` compiles to `mov rax, QWORD PTR [rdi+32]` `mov DWORD PTR [rax], esi`. An extra layer of pointer-chasing is a Bad Thing. Your answer should point out that the downside is far more than just wasted space. – Peter Cordes Nov 14 '16 at 17:45
  • Possibly there's a way to write this where the a b c members don't look like functions, but any indirection optimizes away at compile time? Probably not fully, though, since even if the compiler knows that `a` is a reference to `array[0]`, the object size still has to include any reference members regardless of the compiler being able to prove their value (e.g. by deleting all constructors so even derived classes couldn't initialize them differently?) – Peter Cordes Nov 14 '16 at 17:47
  • @PeterCordes I think I found a solution which is close to the optimum using `operator[]`: https://godbolt.org/g/HZvXMV. It does not use extra members, so the size of the struct does not increase. It uses an additional indirection through a member pointer. – Jens Nov 14 '16 at 20:02
  • @Jens: hmm, that's interesting. It gives you a single static array shared by all instances. I don't know why compilers aren't optimizing it away, since the array is inside a member function, not something that a derived class could modify. Again looks like a missed-optimization, not an inherent problem with the source (unlike having reference members). It's maybe something you could consider actually using, if you really need `foo.a` to keep working without changing it to `foo.a()`, and `operator[]` is infrequently used. – Peter Cordes Nov 14 '16 at 20:42
  • This is MUCH better than the accepted solution to my question ~6 months ago: http://stackoverflow.com/questions/37311006/iterate-through-struct-by-variable-name – dberm22 Nov 15 '16 at 16:26
15

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;
StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
11

In ISO C99/C11, union-based type-punning is legal, so you can use that instead of indexing pointers to non-arrays (see various other answers).

ISO C++ doesn't allow union-based type-punning. GNU C++ does, as an extension, and I think some other compilers that don't support GNU extensions in general do support union type-punning. But that doesn't help you write strictly portable code.

With current versions of gcc and clang, writing a C++ member function using a switch(idx) to select a member will optimize away for compile-time constant indices, but will produce terrible branchy asm for runtime indices. There's nothing inherently wrong with switch() for this; this is simply a missed-optimization bug in current compilers. They could compiler Slava' switch() function efficiently.


The solution/workaround to this is to do it the other way: give your class/struct an array member, and write accessor functions to attach names to specific elements.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

We can have a look at the asm output for different use-cases, on the Godbolt compiler explorer. These are complete x86-64 System V functions, with the trailing RET instruction omitted to better show what you'd get when they inline. ARM/MIPS/whatever would be similar.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

By comparison, @Slava's answer using a switch() for C++ makes asm like this for a runtime-variable index. (Code in the previous Godbolt link).

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

This is obviously terrible, compared to the C (or GNU C++) union-based type punning version:

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • @M.M: good point. It's more of an answer to various comment, and an alternative to Slava's answer. I re-worded the opening bit, so it at least starts off as an answer to the original question. Thanks for pointing that out. – Peter Cordes Nov 15 '16 at 08:06
  • While union-based type punning seems to work in gcc and clang while using the `[]` operator directly on a union member, the Standard defines `array[index]` as being equivalent to `*((array)+(index))`, and neither gcc nor clang will reliably recognize that an access to `*((someUnion.array)+(index))` is an access to `someUnion`. The only explanation I can see is that `someUnion.array[index]` nor `*((someUnion.array)+(index))` aren't defined by the Standard, but are merely a popular extensions, and gcc/clang have opted not to support the second but seem to support the first, at least for now. – supercat Jul 08 '19 at 15:49
9

In C++, this is mostly undefined behavior (it depends on which index).

From [expr.unary.op]:

For purposes of pointer arithmetic (5.7) and comparison (5.9, 5.10), an object that is not an array element whose address is taken in this way is considered to belong to an array with one element of type T.

The expression &thing.a is thus considered to refer to an array of one int.

From [expr.sub]:

The expression E1[E2] is identical (by definition) to *((E1)+(E2))

And from [expr.add]:

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.

(&thing.a)[0] is perfectly well-formed because &thing.a is considered an array of size 1 and we're taking that first index. That is an allowed index to take.

(&thing.a)[2] violates the precondition that 0 <= i + j <= n, since we have i == 0, j == 2, n == 1. Simply constructing the pointer &thing.a + 2 is undefined behavior.

(&thing.a)[1] is the interesting case. It doesn't actually violate anything in [expr.add]. We're allowed to take a pointer one past the end of the array - which this would be. Here, we turn to a note in [basic.compound]:

A value of a pointer type that is a pointer to or past the end of an object represents the address of the first byte in memory (1.7) occupied by the object53 or the first byte in memory after the end of the storage occupied by the object, respectively. [ Note: A pointer past the end of an object (5.7) is not considered to point to an unrelated object of the object’s type that might be located at that address.

Hence, taking the pointer &thing.a + 1 is defined behavior, but dereferencing it is undefined because it does not point to anything.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Evaluating (&thing.a) + 1 is _just about_ legal because a pointer past the end of an array is legal; reading or writing the data stored there is undefined behaviour, comparing with &thing.b with <, >, <=, >= is undefined behaviour. (&thing.a) + 2 is absolutely illegal. – gnasher729 Nov 16 '16 at 11:51
  • @gnasher729 Yeah it's worth clarifying the answer some more. – Barry Nov 16 '16 at 15:03
  • The `(&thing.a + 1)` is an interesting case I failed to cover. +1! ... Just curious, are you on the ISO C++ committee? – WhiZTiM Nov 16 '16 at 15:51
  • It's also a very important case because otherwise every loop using pointers as a half-open interval would be UB. – Jens Nov 17 '16 at 08:06
  • Regarding the last standard citation. C++ is must better specified than C here. – 2501 Nov 18 '16 at 21:17
8

This is undefined behavior.

There are lots of rules in C++ that attempt to give the compiler some hope of understanding what you are doing, so it can reason about it and optimize it.

There are rules about aliasing (accessing data through two different pointer types), array bounds, etc.

When you have a variable x, the fact that it isn't a member of an array means that the compiler can assume that no [] based array access can modify it. So it doesn't have to constantly reload the data from memory every time you use it; only if someone could have modified it from its name.

Thus (&thing.a)[1] can be assumed by the compiler to not refer to thing.b. It can use this fact to reorder reads and writes to thing.b, invalidating what you want it to do without invalidating what you actually told it to do.

A classic example of this is casting away const.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

here you typically get a compiler saying 7 then 2 != 7, and then two identical pointers; despite the fact that ptr is pointing at x. The compiler takes the fact that x is a constant value to not bother reading it when you ask for the value of x.

But when you take the address of x, you force it to exist. You then cast away const, and modify it. So the actual location in memory where x is has been modified, the compiler is free to not actually read it when reading x!

The compiler may get smart enough to figure out how to even avoid following ptr to read *ptr, but often they are not. Feel free to go and use ptr = ptr+argc-1 or somesuch confusion if the optimizer is getting smarter than you.

You can provide a custom operator[] that gets the right item.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

having both is useful.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • "the fact that it isn't a member of an array means that the compiler can assume that no [] based array access can modify it." - not true, e.g. `(&thing.a)[0]` may modify it – M.M Nov 15 '16 at 08:06
  • I don't see how the const example has anything to do with the question. That fails only because there is a specific rule that a const object may not be modified, not any other reason. – M.M Nov 15 '16 at 08:07
  • 1
    @M.M, it's not an example of indexing into a struct, but it's a *very* good illustration of how using undefined behavior to reference something by its *apparent* location in memory, can result in different output than expected, because the compiler can *do something else* with the UB than you wanted it to. – Wildcard Nov 15 '16 at 09:44
  • @M.M Sorry, no array access other than a trivial one through a pointer to the object itself. And the second one is just an example of easy to see side effects of undefined behavior; the compiler optimizes out the reads to `x` because it *knows* you cannot change it in a defined way. Similar optimization could occur when you alter `b` via `(&blah.a)[1]` if the compiler can prove there was no defined access to `b` that could alter it; such a change could occur due to seeminly innocuous changes in compiler, surrounding code, or whatever. So even *testing* that it works isn't sufficient. – Yakk - Adam Nevraumont Nov 15 '16 at 14:52
6

Heres a way to use a proxy class to access elements in a member array by name. It is very C++, and has no benefit vs. ref-returning accessor functions, except for syntactic preference. This overloads the -> operator to access elements as members, so to be acceptable, one needs to both dislike the syntax of accessors (d.a() = 5;), as well as tolerate using -> with a non-pointer object. I expect this might also confuse readers not familiar with the code, so this might be more of a neat trick than something you want to put into production.

The Data struct in this code also includes overloads for the subscript operator, to access indexed elements inside its ar array member, as well as begin and end functions, for iteration. Also, all of these are overloaded with non-const and const versions, which I felt needed to be included for completeness.

When Data's -> is used to access an element by name (like this: my_data->b = 5;), a Proxy object is returned. Then, because this Proxy rvalue is not a pointer, its own -> operator is auto-chain-called, which returns a pointer to itself. This way, the Proxy object is instantiated and remains valid during evaluation of the initial expression.

Contruction of a Proxy object populates its 3 reference members a, b and c according to a pointer passed in the constructor, which is assumed to point to a buffer containing at least 3 values whose type is given as the template parameter T. So instead of using named references which are members of the Data class, this saves memory by populating the references at the point of access (but unfortunately, using -> and not the . operator).

In order to test how well the compiler's optimizer eliminates all of the indirection introduced by the use of Proxy, the code below includes 2 versions of main(). The #if 1 version uses the -> and [] operators, and the #if 0 version performs the equivalent set of procedures, but only by directly accessing Data::ar.

The Nci() function generates runtime integer values for initializing array elements, which prevents the optimizer from just plugging constant values directly into each std::cout << call.

For gcc 6.2, using -O3, both versions of main() generate the same assembly (toggle between #if 1 and #if 0 before the first main() to compare): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif
Christopher Oicles
  • 3,017
  • 16
  • 11
  • Nifty. Upvoted mainly because you proved that this optimizes away. BTW, you can do that much more easily by writing a very simple function, not a whole `main()` with timing functions! e.g. `int getb(Data *d) { return (*d)->b; }` compiles to just `mov eax, DWORD PTR [rdi+4]` / `ret` (https://godbolt.org/g/89d3Np). (Yes, `Data &d` would make the syntax easier, but I used a pointer instead of ref to highlight the weirdness of overloading `->` this way.) – Peter Cordes Nov 15 '16 at 08:21
  • Anyway, this is cool. Other ideas like `int tmp[] = { a, b, c}; return tmp[idx];` don't optimize away, so it's neat that this one does. – Peter Cordes Nov 15 '16 at 08:22
  • One more reason I miss `operator.` in C++17. – Jens Nov 15 '16 at 21:58
2

If reading values is enough, and efficiency is not a concern, or if you trust your compiler to optimize things well, or if struct is just that 3 bytes, you can safely do this:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

For C++ only version, you would probably want to use static_assert to verify that struct data has standard layout, and perhaps throw exception on invalid index instead.

hyde
  • 60,639
  • 21
  • 115
  • 176
1

It is illegal, but there is a workaround:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Now you can index v:

Sven Nilsson
  • 1,861
  • 10
  • 11
  • This is not UB in C but UB in C++, and the question is more focused on C++ – user3528438 Nov 14 '16 at 14:01
  • 6
    Many c++ projects think downcasting all over the place is just fine. We still shouldn't preach bad practices. – StoryTeller - Unslander Monica Nov 14 '16 at 14:04
  • @user3528438 : does not c++ also have the **common initial sequence** rule? – sp2danny Nov 14 '16 at 14:09
  • 2
    The union solves the strict aliasing issue in both languages. But type punning through unions is only fine in C, not in C++. – Lundin Nov 14 '16 at 14:11
  • 1
    still, I wouldn't be surprised if this works on 100% of all c++ compilers. ever. – Sven Nilsson Nov 14 '16 at 14:30
  • 1
    You could try it in gcc with the most aggressive optimizer settings on. – Lundin Nov 14 '16 at 14:43
  • my guess is that it would depend on how you use the struct and assign its members. if the compiler thinks it can solve the task without using any struct at all, you are screwed of course :) – Sven Nilsson Nov 14 '16 at 15:17
  • @SvenNilsson: That's a risky guess - optimizers pull all sorts of weird and wonderful tricks. – Martin Bonner supports Monica Nov 14 '16 at 15:31
  • 1
    @Lundin: union type punning is legal in **GNU** C++, as an extension over ISO C++. It doesn't seem to be stated very clearly in [the manual](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Type%2Dpunning), but I'm pretty sure about this. Still, this answer needs to explain where it's valid and where it isn't. – Peter Cordes Nov 14 '16 at 17:13