0

Here's the test code:

#include <stdio.h>

struct test
{
   int m[1];
};

struct test2: public test
{
   int m1[22];
   void set(int x, int y) { m[x] = y; }
};

int main()
{
    test2 t;
    t.m[1] = 123;
    t.set(0, 0);
    t.set(1, 1);
    printf("%d %d\n", t.m[0], t.m[1]);
    return 0;
}

I compile it once without and once with optimization:

$ g++ -O0 testf.cpp 
$ ./a.out 
0 1
$ g++ -O2 testf.cpp 
$ ./a.out 
1 123

It seems to me that gcc sees array size m[1] and optimizes access to it to be always to the first element m[0]. The question is: is it optimization bug or, some C++ rule is broken so that gcc can do what it does, and if so then what rule?

Note that no memory/stack overrun takes place because of extra m1[22] memory (which was by design in the real app). I don't ask if that's a good programming style, I'm just curious to get the correct answer to the question above.

UPDATE: I accepted the answer with std details, but the biggest help was the comment with the following link: Is the "struct hack" technically undefined behavior?

Community
  • 1
  • 1
queen3
  • 15,333
  • 8
  • 64
  • 119
  • As a test, if you write `void set(int x, int y) { x[(int*)m] = y; }` does the troublesome optimization cease? I suspect this changes it from "array object access" to "dereferencing a valid but unsafely-derived pointer value". – Ben Voigt May 03 '13 at 18:54
  • No, only if I do (volatile int*). – queen3 May 03 '13 at 18:56

2 Answers2

12

Your program has undefined behavior. Here:

t.m[1] = 123;

You are writing to an out-of-bounds location (m is an array of one element, and you are indexing a non-existing second element), and the same is true for:

t.set(1, 1);

Since it basically ends up doing:

m[1] = 1;

You cannot expect anything from a program with undefined behavior - especially not a consistent behavior.

Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • This is a common style, e.g. see BITMAPINFO - bmiColors has array of size [1] but we access it with any index. So my question is, why exactly is this wrong? E.g. some quote from standard, etc. – queen3 May 03 '13 at 18:16
  • 2
    @queen3: If `m` has one element, you cannot access `m[1]`. Array indices are zero-based, so it should be `m[0]`. – Andy Prowl May 03 '13 at 18:17
  • So I can't use bmiColors in BITMAPINFO? – queen3 May 03 '13 at 18:17
  • 1
    @queen3 You can use index 0. But no other index. – Daniel Fischer May 03 '13 at 18:18
  • @queen3: I don't know how that is defined, but if `bmiColors` is an array of 1 element, you definitely should not access it as `bmiColors[1]`, `bmiColors[2]`, or anything else than `bmiColors[0]` – Andy Prowl May 03 '13 at 18:18
  • 1
    @queen3: Possibly, unless it relies on some MS-specific and documented compiler behavior. Definitely this is UB in Standard C++. – Andy Prowl May 03 '13 at 18:21
  • @queen3: Windows requires relaxed pointer safety, which makes it work. – Ben Voigt May 03 '13 at 18:21
  • 1
    @queen3: [The idiom there is totally different than what you have](http://stackoverflow.com/questions/3711233/is-the-struct-hack-technically-undefined-behavior). It is also undefined behavior, but granted it is more common (still bad practice!). You can't just copy some other code and pretend you get to hide behind some shield of "well they did it". – GManNickG May 03 '13 at 18:23
  • Can you elaborate more on "relaxed pointer safety"? I'm really curious if that's some known settings/switches or just MS specific compiler behavior? – queen3 May 03 '13 at 18:23
  • 1
    that is not just a windows approach. This is common practice if you have to define a structure with data that length you did not know.usually the actual length of the data behind the structure is mentioned in the strucutre header. – vlad_tepesch May 03 '13 at 18:24
  • @GManNickG The idiom of "struct hack" is what exactly is used in the real app (they also use a stack-based access as in my example, too), still generating the same UB and optimization bug. Thanks for the link, it's not really different from what I have. – queen3 May 03 '13 at 18:27
  • 1
    @queen3 Does it have the inheritance though? Inheritance will almost certainly make the struct hack not work. – David Brown May 03 '13 at 18:32
  • The inheritance is the same as in the example, just sometimes they instantiate it on stack with test2, and sometimes they do malloc() to fit required number of ints into m[] (and of course they do #pragma pack) and then cast to test2*. But all of this does not really affect the bug, it still happens in the real app, so I believe that the example is correct to reproduce the problem. – queen3 May 03 '13 at 18:35
  • 1
    @queen3 the link is different than what you have then. You can't use this trick and use inheritance. I would only expect this hack to work on [Plain Old Data](http://stackoverflow.com/questions/146452/what-are-pod-types-in-c) types. – David Brown May 03 '13 at 18:39
  • Not really, I removed test2 and inheritance and put both m/m2 and set() into the test struct (and it is POD), the bug still happens. – queen3 May 03 '13 at 18:42
  • 1
    @queen3: You missed my point, which is: don't do this, it's ugly and there's no reason to try to copy the ugly code others have already written. Considering the amount of trouble you've already gone through, perhaps a more straightforward solution should be used? – GManNickG May 03 '13 at 18:44
  • "Since it basically ends up doing: m[1] = 1;" - well, it DOES assign 123 to m[1], so it does not always do what you say. – queen3 May 03 '13 at 18:45
  • "I don't ask if that's a good programming style" because I know it's not. I don't advocate for it. I just want to know all the internal rules that apply here, because I feel that I don't grok it completely. – queen3 May 03 '13 at 18:46
10

Here's the relevant rule, from 5.7:

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 both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

Recall that t.m[i] is equivalent to *(t.m+i) and therefore the pointer addition rules come into play.

Clearly the pointer operand t.m and the result (t.m + 1) do not point to members of the same array object. However, in this case the result is "one past the last element of the array object". So the pointer is valid, but cannot be dereferenced under strict pointer safety rules. Since you are attempting to dereference it, you are back to undefined behavior.

Note that there is no guarantee that t.m + 1 == t.m1, since the compiler is permitted to insert padding between base subobjects and members.

Note also that the compiler WOULD BE required to generate memory access to the correct location for the expression reinterpret_cast<int*>(reinterpret_cast<intptr_t>(t.m) + i * sizeof (int)), unless it defines __STDCPP_STRICT_POINTER_SAFETY__. But it's not specified how that will overlap with the m1 array. And you could be overwriting some sort of magic metadata written there by the compiler (more likely with polymorphic types, of course).

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • "compiler IS required to generate memory access to the correct location" - could you provide more info, a link or likewise? I tried to print get_pointer_safety() (with #include memory) but this is not known to g++ even with -std=c++11/c++0x, and the macro is also unknown. – queen3 May 03 '13 at 18:33
  • 1
    @queen3: Here's the definition "An implementation may have relaxed pointer safety, in which case the validity of a pointer value does not depend on whether it is a safely-derived pointer value. Alternatively, an implementation may have strict pointer safety, in which case a pointer value that is not a safely-derived pointer value is an invalid pointer value unless the referenced complete object is of dynamic storage duration and has previously been declared reachable" – Ben Voigt May 03 '13 at 18:38
  • Hm, well, with reinterpret_cast, sure it will access the correct element because this is no longer a fixed-size array access. – queen3 May 03 '13 at 18:40