https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116469

            Bug ID: 116469
           Summary: Inconsistent Zero Initialization of Nested Structures
           Product: gcc
           Version: 14.1.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: jonassonarvid02 at gmail dot com
  Target Milestone: ---

Created attachment 58980
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=58980&action=edit
Example 1 preprocessed

Description:
Following up on Bug 112666, I've discovered inconsistencies in GCC's
zero-initialization behavior for structs containing subobjects with
user-provided default constructors. The behavior varies depending on the
struct's composition and the size of arrays within inner structs, contradicting
the expected behavior based on the C++ standard and the previous bug
discussion.

-----------------------------------Examples-----------------------------------
Example 1:
------------------------------------------------------------------------------
#include <iostream>

struct Inner {
    Inner(){}
    unsigned char arr[10];
};

// Struct 1: Zero-initialized
struct Outer1 {
    int dummy;
    Inner inner;
};

// Struct 2: Not zero-initialized
struct Outer2 {
    Inner inner;
};

// Struct 3: Not zero-initialized
struct Outer3 {
    Inner inner;
    int dummy;
};

int main() {
    std::cout << "Outer1:\n";
    for(int i = 0; i < 2; ++i) {
        unsigned char counter = 0;
        Outer1 outer{};
        for(auto &c : outer.inner.arr) {
            std::cout << int(c) << ' ';
            c = counter++;
        }
        std::cout << '\n';
    }

    std::cout << "Outer2:\n";
    for(int i = 0; i < 2; ++i) {
        unsigned char counter = 0;
        Outer2 outer{};
        for(auto &c : outer.inner.arr) {
            std::cout << int(c) << ' ';
            c = counter++;
        }
        std::cout << '\n';
    }

    std::cout << "Outer3:\n";
    for(int i = 0; i < 2; ++i) {
        unsigned char counter = 0;
        Outer3 outer{};
        for(auto &c : outer.inner.arr) {
            std::cout << int(c) << ' ';
            c = counter++;
        }
        std::cout << '\n';
    }
}
------------------------------------------------------------------------------


Example 2:
------------------------------------------------------------------------------
#include <iostream>
#include <utility>
#include <vector>

template<unsigned int N>
struct Inner {
    Inner() {}
    unsigned char arr[N];
};


struct Outer1 {
    template<unsigned int N>
    struct Outer {
        int dummy;
        Inner<N> inner;
    };
};

struct Outer2 {
    template<unsigned int N>
    struct Outer {
        Inner<N> inner;
    };
};

struct Outer3 {
    template<unsigned int N>
    struct Outer {
        Inner<N> inner;
        int dummy;
    };
};

template<typename T, unsigned int N>
bool isZeroInit() {
    for(int i = 0; i < 2; i++) {
        typename T::template Outer<N> outer{};
        for(auto &c : outer.inner.arr) {
            if(c != 0) {
                return false;
            }
            c = 1;
        }
    }
    return true;
}

template <typename T, unsigned int N>
auto checkZeroInit(std::vector<bool> v, std::integer_sequence<unsigned int, N>)
{
    if constexpr (N != 0)
        v.push_back(isZeroInit<T, N>());
    return v;
}

template <typename T, unsigned int N, unsigned int... M>
auto checkZeroInit(std::vector<bool> v, std::integer_sequence<unsigned int, N,
M...>) {
    if constexpr (N != 0)
        v.push_back(isZeroInit<T, N>());
    return checkZeroInit<T>(std::move(v), std::integer_sequence<unsigned int,
M...>{});
}


int main() {
    auto v = checkZeroInit<Outer1>(std::vector<bool>{},
std::make_integer_sequence<unsigned int, 300>{});
    std::cout << "Outer1: ";
    for(auto b : v)
        std::cout << b;
    std::cout << std::endl;

    v = checkZeroInit<Outer2>(std::vector<bool>{},
std::make_integer_sequence<unsigned int, 300>{});
    std::cout << "Outer2: ";
    for(auto b : v)
        std::cout << b;
    std::cout << std::endl;

    v = checkZeroInit<Outer3>(std::vector<bool>{},
std::make_integer_sequence<unsigned int, 300>{});
    std::cout << "Outer3: ";
    for(auto b : v)
        std::cout << b;
    std::cout << std::endl;
    return 0;
}
------------------------------------------------------------------------------


Expected Behavior:
According to the C++ standard and the discussion in Bug 112666, structs without
user-provided constructors should have all their members zero-initialized
during value initialization, regardless of the struct's composition or the size
of array members.

Actual Behavior:
The zero-initialization behavior is inconsistent and depends on:
 * The struct's composition (presence and position of other members)
 * The size of array members within inner structs

Observations:
Outer1 (int member before Inner): Inconsistent for most array lengths,
consistent only for larger arrays
Outer2 (only Inner member): Zero-initialized only for small array sizes
Outer3 (Inner member before int): Zero-initialized for small and large array
sizes, but not for medium sizes


-----------------------------------Outputs-----------------------------------
arvidjonasson@Arvids-MacBook-Air ~/testZeroInit
% g++-14 -O3 -std=c++11 -Wall -Wextra example1.cpp -o example1.out
arvidjonasson@Arvids-MacBook-Air ~/testZeroInit
% ./example1.out                                                  
Outer1:
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
Outer2:
0 0 0 0 0 1 2 3 4 5 
0 1 2 3 4 5 6 7 8 9 
Outer3:
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9

arvidjonasson@Arvids-MacBook-Air ~/testZeroInit
% g++-14 -O3 -std=c++17 -Wall -Wextra example2.cpp -o example2.out
---[ lots of -Wmaybe-uninitialized warnings ]---
    inlined from 'int main()' at example2.cpp:77:30:
example2.cpp:40:18: warning:
'outer.Outer3::Outer<17>::inner.Inner<17>::arr[16]' may be used uninitialized
[-Wmaybe-uninitialized]
   40 |             if(c != 0) {
      |                ~~^~~~
example2.cpp: In function 'int main()':
example2.cpp:38:39: note: 'outer' declared here
   38 |         typename T::template Outer<N> outer{};
      |         
arvidjonasson@Arvids-MacBook-Air ~/testZeroInit
% ./example2.out                                                  
Outer1:
11010000000000000110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011101110111011111111111111111111111111111111111111111111111
Outer2:
11111111111111101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Outer3:
11111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111111111111111111111111111

------------------------------------------------------------------------------

Additional Notes:
This issue expands on Bug 112666, revealing a more complex behavior than
initially discussed. The observed dependency on array size and struct
composition raises important questions about the correct interpretation of the
C++ standard in these cases:

 * Should the size of an array member influence whether its containing struct
is zero-initialized?
 * Does the standard specify different behavior for structs with different
compositions (e.g., order of members, presence of non-array members) during
zero-initialization?
 * Is the current behavior a bug in GCC or an intentional implementation detail
that adheres to a specific interpretation of the standard?

These inconsistencies could lead to subtle, hard-to-diagnose bugs, especially
in performance-critical code. Clarification on the expected behavior would be
greatly appreciated.

Compiler and system information:
% g++-14 -v
Using built-in specs.
COLLECT_GCC=g++-14
COLLECT_LTO_WRAPPER=/opt/homebrew/Cellar/gcc/14.1.0_2/bin/../libexec/gcc/aarch64-apple-darwin23/14/lto-wrapper
Target: aarch64-apple-darwin23
Configured with: ../configure --prefix=/opt/homebrew/opt/gcc
--libdir=/opt/homebrew/opt/gcc/lib/gcc/current --disable-nls
--enable-checking=release --with-gcc-major-version-only
--enable-languages=c,c++,objc,obj-c++,fortran,m2 --program-suffix=-14
--with-gmp=/opt/homebrew/opt/gmp --with-mpfr=/opt/homebrew/opt/mpfr
--with-mpc=/opt/homebrew/opt/libmpc --with-isl=/opt/homebrew/opt/isl
--with-zstd=/opt/homebrew/opt/zstd --with-pkgversion='Homebrew GCC 14.1.0_2'
--with-bugurl=https://github.com/Homebrew/homebrew-core/issues
--with-system-zlib --build=aarch64-apple-darwin23
--with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 14.1.0 (Homebrew GCC 14.1.0_2)

Reply via email to