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


135

このスタックオーバーフローの質問に対する受け入れられた(そして唯一の)回答によると

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

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()がゼロ初期化を実行するように指定されていました。

デフォルトのコンストラクターデフォルトの初期化を小さなものではありますが)頭に入れようとしましたが、説明がつきませんでした。


3
興味深いことに、b:main.cpp:18:34:warning: 'b.bar::b'がこの関数で初期化されずに使用されています[-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl

8
barのコンストラクターはユーザー指定fooですが、のコンストラクターはデフォルトのコンストラクターです。
Jarod42

2
@PeteBecker、私はそれを理解しています。どうすればRAMを少し振って、ゼロがあったとしても、それが別のものになるはずです。;)psプログラムを数十回実行しました。大きなプログラムではありません。それを実行して、システムでテストすることができます。aゼロです。bではありません。思わa初期化されます。
ダックドジャース

2
@JoeyMallone「ユーザーがどのように提供するか」に関して:の定義bar::bar()がに表示される保証はありません。main()別のコンパイルユニットで定義されmain()、宣言のみが表示されているときに非常に重要な処理を行う場合があります。この動作はbar::bar()、の定義を別のコンパイルユニットに配置するかどうかによって変化しないことに同意するでしょう(全体の状況が直感的でない場合でも)。
Max Langhof

2
@balkiそれともint a = 0;あなたは本当に明示的になりたいですか?
NathanOliver

回答:


109

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

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が自明でないデフォルトコンストラクターである場合、オブジェクトはデフォルトで初期化されます。

適用されなくなり、値の初期化ではなく、[dcl.init] /8.1bに従ってデフォルトで初期化します。

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


52
私が意味する(*_*)、その後ハレルヤ、でも言語の基本的な構文を使用する場合は......、私は言語のドラフトの細かいプリントを読んでする必要があります!しかし、それはおそらくあなたの言うことのようです。
ダックドジャース

12
@balkiはい、bar::bar() = defaultアウトオブラインを実行することはbar::bar(){}インラインを実行することと同じです。
NathanOliver

15
@JoeyMalloneええ、C ++はかなり複雑になる可能性があります。その理由はよくわかりません。
NathanOliver

3
前の宣言がある場合、デフォルトのキーワードを使用した後続の定義では、メンバーはゼロ初期化されません。正しい?これは正しいです。ここで起こっていることです。
NathanOliver

6
その理由は、あなたの引用の中にあります。アウトオブラインのデフォルトのポイントは、「効率的な実行と簡潔な定義を提供しながら、進化するコードベースへの安定したバイナリインターフェースを有効にする」ことです。つまり、 ABIを壊すことなく、必要に応じて後でユーザーが作成した本文。アウトオブラインの定義は暗黙的にインラインではないため、デフォルトでは1つのTUにしか表示できないことに注意してください。クラス定義だけを見る別のTUは、デフォルトとして明示的に定義されているかどうかを知る方法がありません。
TC

25

動作の違いは、によると[dcl.fct.def.default]/5bar::bar1ではない場合にユーザーが指定するという事実によるものです。その結果、なり初期化値(意味:そのメンバーをゼロ初期化を)が、初期化されていないままになります2foo::foofoo::foo foo::abar::bar


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

ある場合、この関数は、ユーザが提供され、ユーザが宣言していない明示的に債務不履行またはその最初の宣言で削除しました。

2)

[dcl.init#6]

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

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

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

  • ...

[dcl.init.list]から:

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

  • ...

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

ヴィットリオ・ロメオの答え


10

cppreferenceから:

集計の初期化により、集計が初期化されます。これはリスト初期化の形式です。

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

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

  • クラスタイプ[切り取り]

    • [切り取り](異なる標準バージョンにはバリエーションがあります)

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

    • [切り取り](両方のクラスに適用されるより多くのルールがあります)

この定義を考えると、fooは集約ですが、集約barはありません(ユーザー提供のデフォルト以外のコンストラクターがあります)。

したがってfooT object {arg1, arg2, ...};は、集約初期化の構文です。

集約初期化の影響は次のとおりです。

  • [切り取り](このケースに関係のない詳細)

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

したがってa.a、値は初期int化されます。つまり、初期化はゼロです。

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

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

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


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

そのため、投稿する前にプログラムを5〜6回実行しましたが、現在は約10回実行しています。b少し前後します。

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

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

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

どうしたらRAMを少し振って、ゼロがあったとしても、それが別のものになるはずです。

C ++では、初期化されていない値をゼロ以外の値にする方法は保証されていません。

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


「デフォルトで定義したデフォルトのコンストラクターは、メンバーを初期化します(メンバー初期化子がないため)。これにより、intの場合、不確定な値が残ります。」->ええ!「ポッドタイプの場合、デフォルトの初期化はゼロ初期化です。」または私は間違っていますか?
ダックドジャース

2
@JoeyMallone PODタイプのデフォルトの初期化は初期化なしです。
NathanOliver

@NathanOliver、それから私はさらに混乱しています。その後、どうやっaて初期化されますか。私はaメンバーのPODのデフォルトの初期化はデフォルトで初期化されていると考えていましたが、ゼロ初期化です。されa、その後ただ幸いにも、常に私がこのプログラムを実行する回数に関係なくは、ゼロを考え出します。
ダックドジャース

@JoeyMallone Then how come a is initialized.それは初期化された値だからです。I was thinking a is default initializedそうではない。
eerorika

3
@JoeyMallone心配しないでください。C ++での初期化から本を作ることができます。あなたがチャンスを手に入れたら、youtubeのCppConは初期化時にいくつかのビデオを持っています(それがどれほど悪いかを指摘しているように)がyoutube.com/watch?v=7DTlWPgX6zs
NathanOliver

0

ええと、私はあなたが提供したスニペットtest.cppをgccとclangと複数の最適化レベルで実行してみました:

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ビルドが乱数、おそらくスタックスペースを読み取っていることがはっきりとわかります。

私はすぐに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+20p
.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;
}

クラン、おっとっと:

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インライン呼び出しとして推定するほどスマートではありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.