IT박스

C ++ zero initialize-왜이 프로그램에서`b`가 초기화되지 않았는데`a`가 초기화 되었습니까?

itboxs 2020. 7. 5. 08:16
반응형

C ++ zero initialize-왜이 프로그램에서`b`가 초기화되지 않았는데`a`가 초기화 되었습니까?


이 스택 오버플로 질문에 대한 (그리고 유일한) 답변에 따르면 ,

로 생성자 정의

MyTest() = default;

대신 객체를 0으로 초기화합니다.

그렇다면 왜 다음을 수행합니까?

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{};
    bar b{};
    std::cout << a.a << ' ' << b.b;
}

이 출력을 생성하십시오.

0 32766

정의 된 두 생성자가 모두 기본값입니까? 권리? 그리고 POD 유형의 경우 기본 초기화는 0 초기화입니다.

그리고에 대한 허용 대답에 따라 이 질문에 ,

  1. POD 멤버가 생성자에서 또는 C ++ 11 클래스 내 초기화를 통해 초기화되지 않은 경우 기본 초기화됩니다.

  2. 답은 스택이나 힙에 관계없이 동일합니다.

  3. C ++ 98 (그리고 나중에는 아님)에서 새로운 int ()가 0 초기화를 수행하도록 지정되었습니다.

기본 생성자기본 초기화 주위에 ( 작지만 ) 머리 를 감싸려고했지만 설명을 얻을 수 없었습니다.


여기서 문제는 매우 미묘합니다. 당신은 그렇게 생각할 것입니다

bar::bar() = default;

컴파일러에서 생성 한 기본 생성자를 제공하지만 실제로는 사용자가 제공 한 것으로 간주됩니다. [dcl.fct.def.default] / 5 상태 :

명시 적 기본 함수와 암시 적으로 선언 된 함수를 통칭하여 기본 함수라고하며 구현시 해당 함수에 대한 암시 적 정의를 제공해야합니다 ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign ]), 삭제 된 것으로 정의 할 수 있습니다. 함수는 사용자가 선언하고 첫 번째 선언에서 명시 적으로 기본값을 지정하거나 삭제하지 않으면 사용자가 제공합니다.사용자가 제공 한 명시 적 기본값 함수 (즉, 첫 번째 선언 후 명시 적으로 기본값이 지정됨)는 명시 적으로 기본값이 지정된 지점에서 정의됩니다. 이러한 기능이 암시 적으로 삭제 된 것으로 정의되면 프로그램이 잘못 구성됩니다. [참고 : 첫 번째 선언 후 함수를 기본값으로 선언하면 효율적인 코드 실행과 간결한 정의를 제공하면서 진화하는 코드 기반에 안정적인 이진 인터페이스를 사용할 수 있습니다. — 끝 참고]

강조 광산

따라서 bar()처음 선언 할 때 기본값을 지정하지 않았으므로 이제 사용자 제공으로 간주됩니다. 그 때문에 [dcl.init] /8.2

T가 사용자 제공 또는 삭제 된 기본 생성자가없는 클래스 유형일 가능성이있는 경우, 오브젝트는 0으로 초기화되고 기본 초기화에 대한 의미 적 제한 조건이 점검되며 T가 중요하지 않은 기본 생성자를 갖는 경우 , 객체는 기본적으로 초기화됩니다.

더 이상 적용되지 않으며 값 초기화가 b아니라 [dcl.init] /8.1에 따라 기본 초기화됩니다.

T가 기본 생성자 ([class.default.ctor])가 없거나 사용자가 제공하거나 삭제 한 기본 생성자가없는 (cv-qualified) 클래스 유형 ([class]) 인 경우 객체는 기본적으로 초기화됩니다. ;


거동의 차이에 따라, 사실에서 비롯 [dcl.fct.def.default]/5, bar::bar사용자 제공foo::foo없는 1 . 결과적으로, foo::foo가치를 초기화 (의미 : 회원 제로 초기화를 foo::a )하지만, bar::bar초기화되지 않은 상태로 유지됩니다 2 .


1) [dcl.fct.def.default]/5

함수는 사용자가 선언 하고 첫 번째 선언에서 명시 적으로 기본값을 지정하거나 삭제하지 않으면 사용자 가 제공합니다 .

2)

가입일 [dcl.init # 6] :

T 유형의 오브젝트를 값으로 초기화하는 것은 다음을 의미합니다.

  • T가 기본 생성자 ([class.ctor])가 없거나 사용자가 제공하거나 삭제 한 기본 생성자가없는 (cv-qualified) 클래스 유형 인 경우 객체는 기본적으로 초기화됩니다.

  • T가 사용자 제공 또는 삭제 된 기본 생성자가없는 클래스 유형일 가능성 이있는 경우 , 오브젝트는 0으로 초기화 되고 기본 초기화에 대한 의미 적 제한 조건이 점검되며 T가 중요하지 않은 기본 생성자를 갖는 경우 , 객체는 기본적으로 초기화됩니다.

  • ...

에서 [dcl.init.list] :

객체의 목록 초기화 또는 T 유형의 참조는 다음과 같이 정의됩니다.

  • ...

  • 그렇지 않으면 이니셜 라이저 목록에 요소가없고 T가 기본 생성자를 가진 클래스 유형 인 경우 객체는 값으로 초기화됩니다.

에서 비토리오 로미오의 대답


에서 cppreference :

집계 초기화는 집계를 초기화합니다. 목록 초기화 형식입니다.

집계는 다음 유형 중 하나입니다.

[한조각]

  • 클래스 유형 [snip]

    • [싹둑] (다른 표준 버전에 대한 변형이 있습니다)

    • 사용자 제공, 상속 또는 명시 적 생성자 없음 (명시 적으로 기본 또는 삭제 된 생성자가 허용됨)

    • [snip] (두 클래스에 적용되는 규칙이 더 있습니다)

이 정의 foo주어지면 집계 bar는 아니지만 (사용자 제공, 기본이 아닌 생성자가 있음).

따라서를위한 foo, T object {arg1, arg2, ...};집계 초기화를위한 구문입니다.

집계 초기화의 효과는 다음과 같습니다.

  • [snip] (이 경우와 관련이없는 일부 세부 정보)

  • 이니셜 라이저 절 수가 멤버 수보다 적거나 이니셜 라이저 목록이 완전히 비어 있으면 나머지 멤버는 값으로 초기화 됩니다.

Therefore a.a is value initialised, which for int means zero initialisation.

For bar, T object {}; on the other hand is value initialisation (of the class instance, not value initialisation of members!). Since it is a class type with a default constructor, the default constructor is called. The default constructor that you defined default initialises the members (by virtue of not having member initialisers), which in case of int (with non-static storage) leaves b.b with an indeterminate value.

And for pod-types, the default initialization is zero-initialization.

No. This is wrong.


P.S. A word about your experiment and your conclusion: Seeing that output is zero does not necessarily mean that the variable was zero initialised. Zero is perfectly possible number for a garbage value.

for that I ran the program maybe 5~6 times before posting and about 10 times now, a is always zero. b changes around a little.

The fact that the value was same multiple times does not necessarily mean that it was initialised either.

I also tried with set(CMAKE_CXX_STANDARD 14). The result was the same.

The fact that result is the same with multiple compiler options doesn't mean that the variable is initialised. (Although in some cases, changing standard version can change whether it is initialised).

How could I somehow shake my RAM a little so that if there was zero there, it should now be something else

There is no guaranteed way in C++ to make uninitialised value value to appear nonzero.

Only way to know that a variable is initialised is to compare program to the rules of the language and verify that the rules say that it is initialised. In this case a.a is indeed initialised.


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<<<std::char_traits<char>>(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 <iostream>

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(<brace-enclosed initializer list>)’
     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.

참고URL : https://stackoverflow.com/questions/54350114/c-zero-initialization-why-is-b-in-this-program-uninitialized-but-a-is-i

반응형