c++macosg++

MacOS clang compiler issue (Apple clang version 17.0.0 (clang-1700.0.13.5))


Is that an issue with the MacOS g++ compiler (when using optimizations)?

Problem is that when allocating with () or {} an object that has an empty constructor, the compiler incorrectly thinks that the object is uninitialized, and thus makes incorrect optimizations.

That is for instance, it passes nothing to a call to printf that will print the object's members.

The compiler however should know that the object's memory has been zeroed out by () or {}, even though the class has an empty constructor, and that it thus cannot ignore the values of the object's members.

I cannot reproduce the issue with linux distros like ubuntu. Seems Apple only problem.

Here is code:

#include "stdio.h"
#include <string.h>


class ThirdPartyWidget {
public:
    ThirdPartyWidget() { /* constructor does nothing, hence members are left uninitialized */ }
    int data;

    void p() {
        // print object values
        printf("this %p (%lu): data: %d\n", this, sizeof(*this), data);
    }

    void px() {
        // dump the object in hexadecimal
        printf("px: this %p (%zu): ", this, sizeof(*this));
        unsigned char * c = (unsigned char *)this;
        for (int i=0; i < sizeof(*this); i++) {
            printf(" %02x", *c++);
        }
        printf("\n");
    }
};



int main() {
    // allocate on the stack, uninitialized
    ThirdPartyWidget w0;


    // NO initialization.
    // 'w1->data' contains uninitialized value, so the
    // compiler is allowed to ignore those values when p()
    // is initially called
    auto w1 = new ThirdPartyWidget;


    // Value initialization with parenthesis.
    // The memory for w2 is *first* zeroed out, even though the constructor does nothing, so 
    // the compiler should know that 'w2->data' is valid.
    auto w2 = new ThirdPartyWidget();


    // List initialization with empty braces.
    // This also performs value initialization.
    auto w3 = new ThirdPartyWidget{};


    printf("\nw0\n");
    // w0 is uninitialized so the MacOS compiler correctly
    // doesn't bother to pass any values to printf
    w0.p();
    w0.px();

    printf("\nw1\n");
    // w1 is uninitialized so the MacOS compiler correctly
    // doesn't bother to pass any values to printf
    w1->p();
    w1->px();


    printf("\nw2\n");
    // at this point the MacOS compiler incorrectly 
    // thinks that the members variables of w2
    // are NOT initialized, so it ignores them and 
    // passes nothing to printf()
    w2->p();
    w2->px();

    printf("\nw3\n");
    // at this point the MacOS compiler incorrectly 
    // thinks that the members variables of w3
    // are NOT initialized, so it ignores them and 
    // passes nothing to printf()
    w3->p();
    w3->px();

    // since px() has been called, the MacOS compiler 
    // knows that the members
    // variables are now valid, so it stops ignoring them
    w3->p();


    return 0;
}

Compiled with:

g++ -O3 -std=c++23 buggy_cpp_initialization.cpp -o buggy_cpp_initialization.bin

And output:

buggy_cpp_initialization with O3 opt

w0
this 0x16f5131dc (4): data: 154385960 <-- OK expected that data is garbage
px: this 0x16f5131dc (4):  02 00 00 00

w1
this 0x6000033b4030 (4): data: 154385960 <-- OK expected that data is garbage
px: this 0x6000033b4030 (4):  00 00 00 00

w2
this 0x6000033b4040 (4): data: 154385960 <-- NOK compiler is wrong, data should be 0
px: this 0x6000033b4040 (4):  00 00 00 00

w3
this 0x6000033b4050 (4): data: 154385960 <-- NOK compiler is wrong, data should be 0
px: this 0x6000033b4050 (4):  00 00 00 00
this 0x6000033b4050 (4): data: 0 <-- OK finally the compiler knows that data is 0

Here is the assembly that wrongly calls printf within p(), only the first two parameters are passed (x19 and x25), that is, parameters 'this' and 'sizeof(*this)'. The third parameter to printf, i.e. 'data' is not pushed onto the stack, hence printf shows garbage:

100000650: a90067f3     stp x19, x25, [sp]
100000654: aa1403e0     mov x0, x20
100000658: 9400002a     bl  0x100000700 <_puts+0x100000700>

Here is the assembly that calls the last printf in call to p(), the one that is correct, it's clear that now the compiler passes the right values to printf, i.e. it pushes three values onto the stack (x25, x8 and x19). This matches the format of :

printf("this %p (%lu): data: %d\n", this, sizeof(*this), data);

Correct assembly code:

1000006c0: b9400268     ldr w8, [x19]
1000006c4: a900a3f9     stp x25, x8, [sp, #0x8]
1000006c8: f90003f3     str x19, [sp]
1000006cc: aa1403e0     mov x0, x20
1000006d0: 9400000c     bl  0x100000700 <_puts+0x100000700>

Compiler

g++ --version
Apple clang version 17.0.0 (clang-1700.0.13.5)
Target: arm64-apple-darwin24.5.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

Solution

  • T() and T{} only zero-initialize the object if the constructor of T is not user-provided (https://eel.is/c++draft/dcl.init.general#9.1).

    I.e. only if the constructor is either implicitly generated, or manually =defaulted in the class body.