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
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 =default
ed in the class body.