web-dev-qa-db-ja.com

C ++ゼロ初期化-このプログラムの「b」は初期化されていないのに、「a」は初期化されているのはなぜですか?

このStack Overflowの質問 の受け入れられた(そして唯一の)回答によると、

コンストラクタを定義する

MyTest() = default;

代わりにオブジェクトをゼロで初期化します。

次に、なぜ次のようになりますか?

#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タイプの場合、デフォルトの初期化はゼロ初期化です。

この質問 の受け入れられた回答によると、

  1. PODメンバーがコンストラクターまたはC++ 11クラス内初期化によって初期化されていない場合、デフォルトで初期化されます。

  2. 答えは、スタックまたはヒープに関係なく同じです。

  3. C++ 98では(その後ではなく)、新しいint()はゼロ初期化を実行するように指定されていました。

tiny)頭をラップしようとしているにもかかわらず デフォルトのコンストラクターデフォルトの初期化 、I説明を思い付くことができませんでした。

131
Duck Dodgers

ここでの問題はかなり微妙です。あなたはそれを考えるだろう

_bar::bar() = default;
_

コンパイラが生成したデフォルトのコンストラクタを提供しますが、それは提供しますが、現在はユーザー提供と見なされます。 [dcl.fct.def.default]/5 状態:

明示的にデフォルト化された関数と暗黙的に宣言された関数は、まとめてデフォルト化された関数と呼ばれ、実装はそれらの暗黙的な定義を提供します([class.ctor] [class.dtor]、[class.copy.ctor]、[class.copy.assign ])、つまり、削除済みとして定義することを意味する場合があります。 関数は、ユーザーが宣言し、最初の宣言で明示的にデフォルトまたは削除されていない場合、ユーザーが提供します。ユーザーが明示的にデフォルトを設定した関数(すなわち、最初の宣言の後に明示的にデフォルト設定されます)は、明示的にデフォルト設定された時点で定義されます。そのような関数が暗黙的に削除済みとして定義されている場合、プログラムは不正な形式です。 [注意:最初の宣言後に関数をデフォルトとして宣言すると、効率的な実行と簡潔な定義を提供しながら、進化するコードベースへの安定したバイナリインターフェイスを実現できます。 —メモを終了]

重点鉱山

したがって、最初に宣言したときにデフォルトのbar()を指定しなかったため、ユーザー提供と見なされるようになりました。そのため [dcl.init] /8.2

tが、ユーザー提供または削除されたデフォルトコンストラクターを持たない(おそらくcv修飾)クラスタイプである場合、オブジェクトはゼロで初期化され、デフォルト初期化のセマンティック制約がチェックされ、Tに重要なデフォルトコンストラクターがある場合、オブジェクトはデフォルトで初期化されます。

もはや適用されず、bを初期化する値ではなく、 [dcl.init] /8.1 ごとにデフォルトで初期化する

tが(おそらくcv修飾された)クラス型([class])であり、デフォルトコンストラクター([class.default.ctor])がないか、ユーザー提供または削除されるデフォルトコンストラクターの場合、オブジェクトはデフォルトで初期化されます;

106
NathanOliver

動作の違いは、 [dcl.fct.def.default]/5bar::barユーザー提供です。ここでfoo::foo ではありません1。結果として、 foo::foovalue-initializeそのメンバーを意味します(つまり:zero-initializefoo::a) だが bar::bar初期化されないままになります2


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

関数はuser-declaredであり、その最初の宣言で明示的にデフォルト設定または削除されていない場合、ユーザーが提供します。

2)

[dcl.init#6] から:

タイプTのオブジェクトを値で初期化するとは、次のことを意味します。

  • tが(おそらくcv修飾された)クラスタイプであり、デフォルトコンストラクター([class.ctor])がないか、ユーザー提供または削除されるデフォルトコンストラクターの場合、オブジェクトはデフォルトで初期化されます。

  • Tが(おそらくcv修飾)ユーザー提供または削除されたデフォルトコンストラクターのないクラスタイプの場合、オブジェクトはゼロで初期化され、デフォルトの初期化のセマンティック制約がチェックされ、Tに重要なデフォルトコンストラクターがある場合、オブジェクトはデフォルトで初期化されます。

  • ...

[dcl.init.list] から:

タイプTのオブジェクトまたは参照のリスト初期化は、次のように定義されます。

  • ...

  • それ以外の場合、初期化子リストに要素がなく、Tがデフォルトコンストラクターを持つクラス型である場合、オブジェクトは値で初期化されます

Vittorio Romeo's answer から

25
YSC

cppreference から:

集計の初期化は、集計を初期化します。これはリスト初期化の形式です。

集約は、次のタイプのいずれかです。

[をちょきちょきと切る]

  • クラス型[snip]

    • [snip](標準バージョンごとにバリエーションがあります)

    • ユーザー提供、継承、または明示的なコンストラクターはありません(明示的にデフォルト化または削除されたコンストラクターは許可されます)

    • [snip](両方のクラスに適用されるルールがさらにあります)

この定義が与えられた場合、fooは集約ですが、barはそうではありません(ユーザー提供のデフォルトではないコンストラクターがあります)。

したがって、fooの場合、T object {arg1, arg2, ...};は集計の初期化の構文です。

集計の初期化の効果は次のとおりです。

  • [中略](このケースに関係のない詳細)

  • 初期化子節の数がメンバーの数より少ない場合、または初期化子リストが完全に空の場合、残りのメンバーはvalue-initializedです。

したがって、a.aは初期化された値であり、intの場合はゼロ初期化を意味します。

barの場合、T object {};は(メンバーの値の初期化ではなく、クラスインスタンスの)値の初期化です。デフォルトのコンストラクターを持つクラス型であるため、デフォルトのコンストラクターが呼び出されます。デフォルトを定義したデフォルトのコンストラクターは、メンバーを初期化します(メンバー初期化子がないため)。これは、int(非静的ストレージ)の場合、b.bに不定値を残します。

また、ポッドタイプの場合、デフォルトの初期化はゼロ初期化です。

いいえ、これは間違っています。


追伸実験と結論について一言:出力がゼロであることは、変数がゼロで初期化されたことを必ずしも意味しません。ゼロは、ガベージ値の完全に可能な数です。

そのため、投稿する前に5〜6回プログラムを実行しましたが、現在は約10回、aは常にゼロです。 bは少し変化します。

値が複数回同じであったという事実は、必ずしも初期化されたことを意味しません。

また、set(CMAKE_CXX_STANDARD 14)を試しました。結果は同じでした。

結果が複数のコンパイラオプションで同じであるという事実は、変数が初期化されることを意味しません。 (場合によっては、標準バージョンを変更すると、初期化されるかどうかが変わる場合があります)。

どうにかしてRAMを少し振って、そこにゼロがあった場合、今度は別のものになるはずです。

C++には、初期化されていない値valueを非ゼロとして表示する保証された方法はありません。

変数が初期化されていることを知る唯一の方法は、プログラムを言語のルールと比較し、ルールが初期化されていることをルールが確認していることです。この場合、a.aは実際に初期化されます。

10
eerorika

Meh、gcc&clangおよび複数の最適化レベルを使用して、_test.cpp_として指定したスニペットを実行してみました。

_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 ]
_

そこで興味深いのは、clang O0 buildが乱数、おそらくスタックスペースを読み取っているということです。

すぐにIDAを確認して、何が起こっているのかを確認しました。

_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;
}
_

さて、bar::bar(bar *this)は何をしますか?

_void __fastcall bar::bar(bar *this)
{
  ;
}
_

うーん、何もない。アセンブリを使用する必要がありました。

_.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
_

ええ、それは何もありません。コンストラクタが基本的に行うことは_this = this_です。しかし、実際にはランダムな初期化されていないスタックアドレスをロードして印刷することを知っています。

2つの構造体の値を明示的に指定するとどうなりますか?

_#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;
}
_

Clangをヒット、おっと:

_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 ]
_

G ++でも同様の運命:

_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 ]
_

したがって、これは実質的に直接の初期化bar b(0)であり、集約の初期化ではないことを意味します。

これはおそらく、明示的なコンストラクター実装を提供しない場合、これが外部シンボルである可能性があるためです。たとえば:

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

コンパイラーは、これを最適化されていない段階でのno-op/anインライン呼び出しとして推測するほど賢くありません。

0
Steve Fan