C++ zero initialization - Why is `b` in this program uninitialized, but `a` is initialized?

前端 未结 4 1276
死守一世寂寞
死守一世寂寞 2020-12-07 16:15

According to the accepted (and only) answer for this Stack Overflow question,

Defining the constructor with

MyTest() = default;
         


        
4条回答
  •  难免孤独
    2020-12-07 16:44

    Meh, I tried running the snippet you provided as test.cpp, through gcc & clang and multiple optimization levels:

    steve@steve-pc /tmp> g++ -o test.gcc.O0 test.cpp
                                                                                  [ 0s828 | Jan 27 01:16PM ]
    steve@steve-pc /tmp> g++ -o test.gcc.O2 -O2 test.cpp
                                                                                  [ 0s901 | Jan 27 01:16PM ]
    steve@steve-pc /tmp> g++ -o test.gcc.Os -Os test.cpp
                                                                                  [ 0s875 | Jan 27 01:16PM ]
    steve@steve-pc /tmp> ./test.gcc.O0
    0 32764                                                                       [ 0s004 | Jan 27 01:16PM ]
    steve@steve-pc /tmp> ./test.gcc.O2
    0 0                                                                           [ 0s004 | Jan 27 01:16PM ]
    steve@steve-pc /tmp> ./test.gcc.Os
    0 0                                                                           [ 0s003 | Jan 27 01:16PM ]
    steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
                                                                                  [ 1s089 | Jan 27 01:17PM ]
    steve@steve-pc /tmp> clang++ -o test.clang.Os -Os test.cpp
                                                                                  [ 1s058 | Jan 27 01:17PM ]
    steve@steve-pc /tmp> clang++ -o test.clang.O2 -O2 test.cpp
                                                                                  [ 1s109 | Jan 27 01:17PM ]
    steve@steve-pc /tmp> ./test.clang.O0
    0 274247888                                                                   [ 0s004 | Jan 27 01:17PM ]
    steve@steve-pc /tmp> ./test.clang.Os
    0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
    steve@steve-pc /tmp> ./test.clang.O2
    0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
    steve@steve-pc /tmp> ./test.clang.O0
    0 2127532240                                                                  [ 0s002 | Jan 27 01:18PM ]
    steve@steve-pc /tmp> ./test.clang.O0
    0 344211664                                                                   [ 0s004 | Jan 27 01:18PM ]
    steve@steve-pc /tmp> ./test.clang.O0
    0 1694408912                                                                  [ 0s004 | Jan 27 01:18PM ]
    

    So that is where it gets interesting, it clearly shows clang O0 build is reading random numbers, presumably stack space.

    I quickly turned up my IDA to see what's happening:

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      __int64 v3; // rax
      __int64 v4; // rax
      int result; // eax
      unsigned int v6; // [rsp+8h] [rbp-18h]
      unsigned int v7; // [rsp+10h] [rbp-10h]
      unsigned __int64 v8; // [rsp+18h] [rbp-8h]
    
      v8 = __readfsqword(0x28u); // alloca of 0x28
      v7 = 0; // this is foo a{}
      bar::bar((bar *)&v6); // this is bar b{}
      v3 = std::ostream::operator<<(&std::cout, v7); // this is clearly 0
      v4 = std::operator<<>(v3, 32LL); // 32 = 0x20 = ' '
      result = std::ostream::operator<<(v4, v6); // joined as cout << a.a << ' ' << b.b, so this is reading random values!!
      if ( __readfsqword(0x28u) == v8 ) // stack align check
        result = 0;
      return result;
    }
    

    Now, what does bar::bar(bar *this) does?

    void __fastcall bar::bar(bar *this)
    {
      ;
    }
    

    Hmm, nothing. We had to resort to using assembly:

    .text:00000000000011D0                               ; __int64 __fastcall bar::bar(bar *__hidden this)
    .text:00000000000011D0                                               public _ZN3barC2Ev
    .text:00000000000011D0                               _ZN3barC2Ev     proc near               ; CODE XREF: main+20↓p
    .text:00000000000011D0
    .text:00000000000011D0                               var_8           = qword ptr -8
    .text:00000000000011D0
    .text:00000000000011D0                               ; __unwind {
    .text:00000000000011D0 55                                            push    rbp
    .text:00000000000011D1 48 89 E5                                      mov     rbp, rsp
    .text:00000000000011D4 48 89 7D F8                                   mov     [rbp+var_8], rdi
    .text:00000000000011D8 5D                                            pop     rbp
    .text:00000000000011D9 C3                                            retn
    .text:00000000000011D9                               ; } // starts at 11D0
    .text:00000000000011D9                               _ZN3barC2Ev     endp
    

    So yeah, it's just, nothing, what the constructor basically does is this = this. But we know that it is actually loading random uninitialized stack addresses and print it.

    What if we explicitly provide values for the two structs?

    #include 
    
    struct foo {
        foo() = default;
        int a;
    };
    
    struct bar {
        bar();
        int b;
    };
    
    bar::bar() = default;
    
    int main() {
        foo a{0};
        bar b{0};
        std::cout << a.a << ' ' << b.b;
    }
    

    Hit up clang, oopsie:

    steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
    test.cpp:17:9: error: no matching constructor for initialization of 'bar'
        bar b{0};
            ^~~~
    test.cpp:8:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion
          from 'int' to 'const bar' for 1st argument
    struct bar {
           ^
    test.cpp:8:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion
          from 'int' to 'bar' for 1st argument
    struct bar {
           ^
    test.cpp:13:6: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
    bar::bar() = default;
         ^
    1 error generated.
                                                                                  [ 0s930 | Jan 27 01:35PM ]
    

    Similar fate with g++ as well:

    steve@steve-pc /tmp> g++ test.cpp
    test.cpp: In function ‘int main()’:
    test.cpp:17:12: error: no matching function for call to ‘bar::bar()’
         bar b{0};
                ^
    test.cpp:8:8: note: candidate: ‘bar::bar()’
     struct bar {
            ^~~
    test.cpp:8:8: note:   candidate expects 0 arguments, 1 provided
    test.cpp:8:8: note: candidate: ‘constexpr bar::bar(const bar&)’
    test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ to ‘const bar&’
    test.cpp:8:8: note: candidate: ‘constexpr bar::bar(bar&&)’
    test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ to ‘bar&&’
                                                                                  [ 0s718 | Jan 27 01:35PM ]
    

    So this means it's effectively a direct initialization bar b(0), not aggregate initialization.

    This is probably because if you do not provide an explicit constructor implementation this could potentially be an external symbol, for example:

    bar::bar() {
      this.b = 1337; // whoa
    }
    

    The compiler isn't smart enough to deduce this as a no-op/an inline call in a non-optimized stage.

提交回复
热议问题