Search code examples
c++iccgcc4

Puzzling introduction of temporary in `* const&` on older C++ compilers


When building on a platform with an older C++ compiler, I noticed unexpected behavior with code that worked fine elsewhere. I'm not sure whether this indicates a bug in older compilers, or some change in the standard, but I have been explicitly building with C++11.

In particular, the older compiler seems to use a temporary for a * const& when I wouldn't expect it to, and that temporary leads to a SIGSEGV when its stack frame gets cleaned up and overwritten.

Here is my best effort at distilling a MWE from the original code. My problem revolves around the constructor of class C:

#include <stdio.h>

struct A {
    int* i;
};

class B {
public:
    int* const& i_ptr_const_ref; // normally not public
    B(int* const& i_ptr) : i_ptr_const_ref(i_ptr) {}
    operator int* const& (void) const { return i_ptr_const_ref; }
};

int null_b = { 0 };
int* null_b_ptr = &null_b;
int* const& null_b_ptr_const_ref = null_b_ptr;

class C {
public:
    B b;
//  C(const A* a_ptr) : b(a_ptr ? a_ptr->i : null_b_ptr_const_ref) {} // this works
    C(A* a_ptr) : b(a_ptr ? a_ptr->i : null_b_ptr_const_ref) {} // this fails
//  C(A* a_ptr) : b(a_ptr ? (int* const&) a_ptr->i : null_b_ptr_const_ref) {} // this works
//  C(A* a_ptr) : b(a_ptr->i) {} // this works
};

int main(void) {
    A a;
    A* a_ptr = &a;
    a_ptr->i = (int*) 42;
    C c(a_ptr);
    printf("c.b.i_ptr_const_ref = %p\n", (void*) c.b.i_ptr_const_ref);
    printf("c.b=                  %p\n", (void*) c.b);
    printf("a_ptr->i=             %p\n", (void*) a_ptr->i);
    return 0;
}

Try it on Compiler Explorer

The values that I print out should all match, but on GCC compilers prior to 5.1 and ICC compilers prior to 18 (I understand that Intel prides itself on "bug-for-bug" compatibility with other compilers), the middle one shows a stack address instead of the expected value. All versions of Clang and ELLCC compilers that I was able to try behave correctly.

The uncommented C constructor is the one I want to use, but it doesn't work correctly. I get the expected result in the MWE if I make the A* a_ptr parameter const, but in the larger code base I can't do that. I also get the expected result if I don't use a ?: in the initializer, or if I explicitly cast a_ptr->i in the initializer as int* const&, but I don't understand why I should have to.

I would have thought that initializing an int* const& with an int* would be fine, but my best guess is that the ?: somehow confused the compiler. Can anybody help me understand whether the older GCC and ICC compilers are incorrect here, or whether there is something about the language that I'm misunderstanding?


Solution

  • This is pretty clearly a compiler bug: certainly an lvalue of type int* can be implicitly converted to int* const&, so the result of the conditional operator should be an lvalue.

    The printf output appears mysterious, but in fact is a straightforward consequence of the bug: even the first read of c.b.i_ptr_const_ref (which is of course a reference that is transparently followed) is reading from the dead temporary, but it has yet to be overwritten and still contains a copy of a.i. After the first printf, that memory has been clobbered and happens to hold a stack address instead.