-2

This is doing my head in! I was doing some experiments with the stack during function calls, and I came across this little anomaly. Compiler is GCC 4.9.2, and -O1, -O2, or -O3 optimization flags. The problem is not present when optimizations are disabled (-O0). The result is the same whether compiling under C++98, C++11 or C++14.

In the example below, I assume that the CDECL calling convention is used for the call to result(), so args should get pushed on the stack from right to left.

#include <iostream>

void result(int a, int b) {
    int* pA = &a;
    int* pB = &b;

    // pA should be equal to pB + sizeof(int) bytes, why does this report "false" 
    // when optimizations are on?
    std::cout << (pA == pB + 1 ? "true" : "false") << '\n';

    // this appears to be true if we dump the pointer addresses.
    std::cout << pA << ", " << pB + 1 << '\n';

    // and still holds water when getting values through indirection.
    std::cout << "a: " << *(pB + 1) << " b: " << *pB << '\n';
}

int main(int argc, char** argv)
{
    result(10, 20);
    return 0;
}

Sample output with optimizations enabled:

false // why?
0x704eca3c088c, 0x704eca3c088c // clearly they're equal
a: 10 b: 20 // and point to the correct arguments on the stack

Sample output with optimizations disabled:

true
0x7e5bd5f34fdc, 0x7e5bd5f34fdc
a: 10 b: 20

What about the compiler optimizations would cause a stack pointer comparison to fail, when their equality can be compared visually and found to be true, optimizations or no?

Edit: I believe this question differs from the suggested duplicate (Is it unspecified behavior to compare pointers to different arrays for equality?) as stacks are traditionally implemented as a contiguous array (obviously with exceptions -- case in point), and the proposed duplicate gives an example of two distinct arrays.

Hunter Mueller
  • 223
  • 2
  • 9
  • 2
    Pointer comparison between different objects is undefined. The optimizer is allowed to assume that undefined behavior never occurs. – Barmar Feb 07 '18 at 23:45
  • 1
    *pA should be equal to pB + sizeof(int) bytes* Why? Why should it be? If the compiler see it fit, it doesn't have to store these values in the stack frame. – Pablo Feb 07 '18 at 23:46
  • That was an assumption on my part. I understand it's undefined behaviour now. Apparently it's undefined unless they're pointers to members of the same object or elements of the same array. – Hunter Mueller Feb 07 '18 at 23:53
  • Possible duplicate of [Is it unspecified behavior to compare pointers to different arrays for equality?](https://stackoverflow.com/questions/4909766/is-it-unspecified-behavior-to-compare-pointers-to-different-arrays-for-equality) – Barmar Feb 07 '18 at 23:56
  • @Barmar `==` and `!=` for pointers is not undefined in C++ – M.M Feb 08 '18 at 00:53
  • @M.M True, it's just unspecified when they point to different objects. – Barmar Feb 08 '18 at 00:54
  • 1
    @Barmar in fact it's guaranteed `false` for pointers to different objects, except the special case of a one-past-the-end pointer from one object and a start pointer for another object which is unspecified – M.M Feb 08 '18 at 01:00
  • 1
    Not sure why this is downvoted, it seems like a good question to me and written well – M.M Feb 08 '18 at 01:28

2 Answers2

1

This result has nothing to do with how the stack is laid out and everything to do with the fact that the result of comparison between unrelated pointers is unspecified.

Lets look at the assembly GCC generates for just the first little bit of your result function:

.LC0:
  .string "false"
.LC1:
  .string ", "
.LC2:
  .string "a: "
.LC3:
  .string " b: "
result(int, int):
  push rbx
  mov edx, 5
  sub rsp, 32
  mov DWORD PTR [rsp+12], edi
  mov DWORD PTR [rsp+8], esi
  mov edi, OFFSET FLAT:std::cout
  mov esi, OFFSET FLAT:.LC0
  call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

As you can see, there is no branch at all. A pointer to the string "false" is loaded into esi and used as the argument to std::__ostream_insert unconditionally. Since the behavior of the pointer comparison is not specified by the language, the compiler has simply assumed that the true case can never happen and removed it entirely.

Miles Budnek
  • 28,216
  • 2
  • 35
  • 52
  • Fascinating. So it was the compiler guarding against undefined behaviour, rather than allowing the outcome to vary from run to run? The sophistication of compiler optimizations never ceases to blow my mind. – Hunter Mueller Feb 08 '18 at 00:24
  • 1
    @HunterMueller It's less that the behavior would vary from run to run and more that it's faster to just ignore the check since the language doesn't specify what should happen. – Miles Budnek Feb 08 '18 at 01:07
1

From C++17 (N4659) [expr.eq]/2 (Equality operators):

Comparing pointers is defined as follows:

  • If one pointer represents the address of a complete object, and another pointer represents the address one past the last element of a different complete object, the result of the comparison is unspecified.
  • [...]

This means comparison of &a and &b+1 is unspecified. (It is not undefined, as claimed in other answers and comments; in fact equality comparison for pointers never undefined behaviour in C++).

So your code may observe either true or false as the result of the comparison; and the optimizer could , at its whim, insert either result instead of actually doing any comparison at runtime.


Historical note: This change came from CWG Defect 1652, filed against C++11 and granted Defect status, meaning it applies retroactively. The original C++11 text said that pointers should compare equal "if they represent the same address" , but this was changed both because it hinders optimization, and because equality comparison is allowed in constexpr which can't do a runtime address check obviously.

I believe g++ always performed this optimization in defiance of the apparent C++11 requirement to not perform it.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • Thanks for the excellent citation and explanation. It's laid out so plainly in the spec, and the retroactivity of the "Defect" state explains why this behaviour is present at least as far back as C++98. I'm choosing this as the answer, as the spec couldn't be clearer. Cheers! – Hunter Mueller Feb 08 '18 at 06:23