https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93018
--- Comment #3 from John Drouhard <john at drouhard dot dev> --- I don't think this is a matter of object lifetime. Passing -fnolifetime-dse or -flifetime-dse=1 does not change the generated assembly here at all. As a user of the functions baz1 and baz2, I would expect the returned object to have identical object representations. Bar's default constructor as well as its user-defined constructor with the null_t argument should produce identical objects here. Even if the copy constructor is used to copy from a temporary object during the return from baz1/baz2, union class types' default copy constructors copy the object representations (as if memmove is used), so we should still be seeing a zero-initialized "foo" member. The assembly generated with -O2, for reference: baz1(long long): xorl %eax, %eax movl $0, %edx testq %rdi, %rdi cmovg %rdi, %rdx setg %al ret baz2(long long): xorl %eax, %eax testq %rdi, %rdi jle .L7 movb $1, %al movq %rdi, %rdx .L7: ret %rax is the bool member of bar, %rdx is the union member. baz2 is simply not storing anything in %rdx, so the caller receives an uninitialized value (instead of zero-initialization) for the union "foo" member. If I write the following program: int main() { Bar obj = baz2(0); printf("%08x\n", *((long long*)&obj.foo)); return 0; } Random garbage is printed out. If I change it to use baz1 instead, 0 is correctly printed. If I change the optimization level to -O3, 0 is correctly printed. If I change the implementation of baz2(long long) to unconditionally return the null sentinel object, 0 is correctly printed. "obj" is also definitely still alive at this point in the program, so object lifetime is not the problem. It seems to only be uninitialized when using the converting constructor in the return statement, using -O2 optimization, and there is another path in the function that returns an object where the active union member is not the empty struct and is initialized.