「変数を常に初期化する」ことで、重要なバグが隠されることはありませんか?


35

C ++コアガイドラインには、ES.20:常にオブジェクトを初期化するというルールがあります

使用前設定エラーとそれに関連する未定義の動作を避けます。複雑な初期化の理解に関する問題を回避します。リファクタリングを簡素化します。

しかし、このルールはバグを見つけるのに役立ちません。バグを隠すだけです。
プログラムに初期化されていない変数を使用する実行パスがあると仮定します。これはバグです。未定義の動作はさておき、それはまた何かがうまくいかなかったことを意味し、プログラムはおそらくその製品要件を満たしていません。本番環境に展開されると、金銭的な損失が発生する可能性があります。

バグをどのように選別しますか?テストを作成します。しかし、テストは実行パスの100%をカバーするわけではなく、テストはプログラム入力の100%をカバーすることはありません。それ以上に、テストでさえ欠陥のある実行パスをカバーします-それはまだパスできます。結局、これは未定義の動作であり、初期化されていない変数はある程度有効な値を持つことができます。

ただし、テストに加えて、0xCDCDCDCDのようなものを初期化されていない変数に書き込むことができるコンパイラーがあります。これにより、テストの検出率がわずかに向上します。
さらに良いことには、初期化されていないメモリバイトのすべての読み取りをキャッチするAddress Sanitizerなどのツールがあります。

最後に、静的アナライザーがあります。これは、プログラムを調べて、その実行パスに事前設定読み取りがあることを伝えることができます。

そのため、多くの強力なツールがありますが、変数を初期化すると、サニタイザーは何も見つかりません

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

別のルールがあります-プログラムの実行でバグが発生した場合、プログラムはできるだけ早く停止する必要があります。そのままにしておく必要はありません。ただクラッシュし、クラッシュダンプを作成して、調査のためにエンジニアに渡してください。
変数を不必要に初期化すると、逆のことが行われます。そうでない場合、プログラムはすでにセグメンテーションフォールトを取得しているはずですが、プログラムは存続しています。


10
これは良い質問だと思いますが、あなたの例がわかりません。読み取りエラーが発生し、bytes_read変更されていない(ゼロが保持されている)場合、なぜこれがバグであると考えられますか?プログラムは、bytes_read!=0その後暗黙的に予期しない限り、正常な方法で続行できます。だから、良い消毒剤は文句を言わない。一方、bytes_read事前に初期化されず、プログラムは正気の方法で継続することができなくなりますので、初期化しないbytes_read、実際に導入し、事前に存在しなかったバグを。
Doc Brown

2
@Abyx:たとえそれがサードパーティであっても、それで始まるバッファを処理しない場合\0はバグがあります。それに対処しないように文書化されている場合、呼び出し元のコードにはバグがあります。あなたがチェックするためにあなたを呼び出すコードを修正した場合bytes_read==0、あなたが開始したところに、使用を呼び出す前に、あなたがしているバック:あなたのコードはバグがある場合は、あなたが初期化しないbytes_read、あなたがしなければ、安全な。(通常、エラーの場合でも関数は出力パラメーターを埋めることになっています。実際にはそうではありません。出力が放置されるか、未定義になることがよくあります。)
Mat

1
このコードがerr_t返すものを無視する理由はありmy_read()ますか?例のどこかにバグがある場合、それだけです。
Blrfl

1
簡単です。意味のある場合にのみ変数を初期化します。そうでない場合は、しないでください。しかし、「ダミー」データを使用してそれを行うのは悪いことです。バグを隠すためです。
ピーターB

1
「別のルールがあります。プログラムの実行でバグが発生した場合、プログラムはできるだけ早く停止する必要があります。それを維持する必要はありません。クラッシュするだけです。制御ソフトウェア。飛行機の残骸からクラッシュダンプを回収してください。
ジョルジオ

回答:


44

あなたの推論はいくつかのアカウントでうまくいきません:

  1. セグメンテーション障害は、確実に発生するものではありません。初期化されていない変数を使用すると、未定義の動作が発生します。セグメンテーションフォールトは、このような動作が現れる可能性のある1つの方法ですが、正常に動作しているように見えることも同様です。
  2. コンパイラは、初期化されていないメモリを定義されたパターン(0xCDなど)で埋めることはありません。これは、一部のデバッガーが初期化されていない変数が使用される場所を見つけるのを支援するために行います。このようなプログラムをデバッガーの外部で実行すると、変数には完全にランダムなゴミが含まれます。同様に、カウンタbytes_readが値10を持つことは、値を持つことと同じです0xcdcdcdcd
  3. 初期化されていないメモリを固定パターンに設定するデバッガで実行している場合でも、起動時にのみそうします。つまり、このメカニズムは、静的(および場合によってはヒープに割り当てられた)変数に対してのみ確実に機能します。スタックに割り当てられるか、レジスターにのみ存在する自動変数の場合、変数が以前に使用された場所に格納される可能性が高いため、テルテールメモリパターンは既に上書きされています。

常に変数を初期化するガイダンスの背後にある考え方は、これらの2つの状況を有効にすることです

  1. 変数には、その存在の最初から有用な値が含まれています。それをガイダンスと組み合わせて、必要なときにだけ変数を宣言する場合、将来のメンテナンスプログラマーが、宣言と最初の割り当ての間で変数の使用を開始するというtrapに陥るのを避けることができます。

  2. この変数には定義済みの値が含まれており、後でテストして、のような関数my_readが値を更新したかどうかを確認できます。初期化がなければ、bytes_read実際に有効な値があるかどうかはわかりません。なぜなら、それがどの値で始まったのかわからないからです。


8
1)1%対99%のように、それはすべて確率です。2および3)VC ++は、ローカル変数に対してもこのような初期化コードを生成します。3)静的は(グローバル)変数は常に0で初期化されている
Abyx

5
@Abyx:1)私の経験では、確率は〜80% "すぐに明らかな行動の違いがない"、10% "間違ったことをする"、10% "セグメンテーション違反"です。(2)および(3)に関しては、VC ++はデバッグビルドでのみこれを行います。リリースビルドを選択的に中断し、多くのテストで表示されないため、これに依存することは非常に悪い考えです。
クリスチャンアイヒンガー

8
「ガイダンスの背後にあるアイデア」がこの答えの最も重要な部分だと思います。ガイダンスは、すべての変数宣言の後にを付けるように指示するものではありません= 0;。アドバイスの目的は、有用な値が得られるポイントで変数を宣言し、すぐにこの値を割り当てることです。これは、すぐ後に続くルールES21およびES22で明示的に明確にされています。これら3つはすべて一緒に機能していると理解する必要があります。個々の無関係なルールとしてではありません。
GrandOpener

1
@GrandOpenerまさに。変数が宣言された時点で割り当てる意味のある値がない場合、変数のスコープはおそらく間違っています。
ケビンクルムウィーデ

5
「コンパイラが埋めることはありません」であってはならないということではない、常に
CodesInChaos

25

「このルールはバグを発見するのに役立ちません。バグを隠すだけです」-ルールの目標はバグの発見を助けることではなく、バグを回避することです。バグが回避されると、何も隠されません。

あなたの例の観点から問題を議論しましょう:my_read関数はbytes_readすべての状況下で初期化するための書面による契約を持っていますが、エラーの場合はそうではないので、少なくともこの場合は欠陥があります。bytes_read最初にパラメーターを初期化しないことで、ランタイム環境を使用してそのバグを表示することを目的としています。アドレスサニタイザーがあることを確実に知っている限り、それは確かにそのようなバグを検出する可能な方法です。バグを修正するには、my_read機能を内部で変更する必要があります。

しかし、少なくとも等しく有効である別の観点では、そこにある:障害のある行動が唯一から出組み合わせ初期化しないのbytes_read事前、および呼び出しmy_read(期待をして、その後bytes_readその後に初期化されます)。これは、次のような関数の記述仕様my_readが100%明確でない場合、またはエラーが発生した場合の動作が間違っている場合に、実際のコンポーネントで頻繁に発生する状況です。ただし、bytes_read呼び出しの前にlong がゼロに初期化されると、プログラムは内部my_readで初期化が行われた場合と同じように動作するため、正しく動作します。この組み合わせでは、プログラムにバグはありません。

したがって、それに続く私の推奨事項は、次の場合にのみ非初期化アプローチを使用します。

  • 関数またはコードブロックが特定のパラメーターを初期化するかどうかをテストしたい
  • あなたは、その関数に値をそのパラメータに割り当てないことは間違いなく間違っている契約を持っていることを100%確信しています
  • あなたは環境がこれをキャッチできることを100%確信しています

これらは、特定のツール環境用にテストコードで通常調整できる条件です。

しかし、本番コードでは、常にそのような変数を事前に初期化することをお勧めします。これは、より防御的なアプローチであり、契約が不完全または間違っている場合、またはアドレスサニタイザーまたは同様の安全対策がアクティブになっていない場合のバグを防ぎます。また、プログラムの実行でバグが発生した場合、正しく記述したように、「クラッシュ初期」ルールが適用されます。しかし、変数を事前に初期化することは何も問題がないことを意味する場合、それ以上の実行を停止する必要はありません。


4
これは、私がそれを読んだときに考えていたものです。じゅうたんの下に物を一掃するのではなく、ゴミ箱に一掃します!
corsiKa

22

常に変数を初期化する

検討している状況の違いは、初期化のない場合は、 ない未定義の動作になり、初期化に時間がかかった場合は明確で確定的なバグが作成されることです。これら2つのケースがどれほど極端に異なるかを強調することはできません。

仮定のシミュレーションプログラムで仮定の従業員に起こった架空の例を考えてみましょう。この仮想チームは仮説的に決定論的シミュレーションを行おうとしており、彼らが仮想的に販売していた製品がニーズを満たしていることを実証していました。

さて、単語の注入で停止します。ポイントを得ると思う;-)

このシミュレーションでは、初期化されていない変数が何百もありました。ある開発者は、シミュレーションでvalgrindを実行し、いくつかの「初期化されていない値の分岐」エラーがあることに気付きました。「うーん、それは非決定性を引き起こす可能性があり、最も必要なときにテストの実行を繰り返すのが難しくなります。」開発者は管理に行きましたが、管理は非常に厳しいスケジュールで行われており、この問題を追跡するためにリソースを割くことができませんでした。「変数を使用する前にすべての変数を初期化することになります。優れたコーディング慣行があります。」

最終納品の数か月前、シミュレーションがフルチャーンモードであり、チーム全体が全力を尽くして、これまでに資金提供されたすべてのプロジェクトと同様に予算が小さすぎる予算で管理が約束したすべてを完了します。何らかの理由で、決定論的シミュレーションがデバッグのために決定論的に動作していないため、重要な機能をテストできないことに気づいた人がいました。

チーム全体が停止し、機能を実装およびテストする代わりに、初期化されていない値のエラーを修正するために、シミュレーションコードベース全体をコーミングするのに2か月の大半を費やした可能性があります。言うまでもなく、従業員は「私はあなたにそう言った」をスキップし、他の開発者が初期化されていない値が何であるかを理解するのをすぐに助けました。不思議なことに、この事件の直後にコーディング標準が変更され、開発者は常に変数を初期化するようになりました。

そして、これは警告ショットです。これはあなたの鼻に放牧された弾丸です。 実際の問題は、あなたが想像しているよりもはるかにはるかに潜んでいます。

初期化されていない値を使用することは「未定義の動作」です(のようないくつかのコーナーケースを除くchar)。未定義の動作(または略してUB)は非常にめちゃくちゃで完全に悪いので、代替手段よりも優れていると決して信じてはいけません。特定のコンパイラがUBを定義していて、安全に使用できることを識別できる場合もありますが、未定義の動作は「コンパイラが感じる動作」です。値が指定されていないような、「正気」と呼ばれる何かをするかもしれません。無効なオペコードが出力され、プログラムが破損する可能性があります。コンパイル時に警告をトリガーしたり、コンパイラがエラーを完全にエラーと見なすことさえあります。

または、何もしないかもしれません

UBの炭鉱の私のカナリアは、私が読んだSQLエンジンの事例です。リンクしないことをお許しください。再度記事を見つけることができませんでした。特定のバージョンのDebianでのみ、より大きなバッファサイズを関数に渡すと、SQLエンジンでバッファオーバーランの問題が発生しました バグは忠実に記録され、調査されました。面白い部分は、バッファオーバーランがチェックされたことです。バッファオーバーランを適切に処理するコードがありました。次のように見えました。

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

レンディションにコメントを追加しましたが、考え方は同じです。場合はput + dataLength周りのラップ、それはより小さくなりますput(彼らは、コンパイル時のチェックは必ずunsigned int型は好奇心のために、ポインタのサイズだったしなければならなかった)ポインタ。これが発生した場合、標準のリングバッファアルゴリズムがこのオーバーフローによって混乱する可能性があることがわかっているため、0を返します。 ます。

結局のところ、ポインターのオーバーフローはC ++では未定義です。ほとんどのコンパイラーはポインターを整数として処理しているため、通常の整数オーバーフロー動作が発生します。ただし、これ未定義の動作です。つまり、コンパイラは何でも実行できます。ことができます。

このバグの場合、Debianはたまたま他の主要なLinuxフレーバーのどれも製品リリースで更新していないgccの新しいバージョンを使用することを選択しました。このgccの新しいバージョンには、より積極的なデッドコードオプティマイザーがありました。コンパイラーは未定義の動作を確認し、ifステートメントの結果は「コードの最適化を最適にするものは何でも」であると判断しました。したがって、UBポインターオーバーフローなしではptr+dataLength下になれないためptrifステートメントはトリガーされない、されず、バッファーオーバーランチェックを最適化する。

「健全な」UBの使用により、実際に主要なSQL製品は、回避するためのコードを記述していたバッファオーバーランを悪用しました。

未定義の動作に依存しないでください。今まで。


未定義の振る舞いに関する非常に面白い読み物として、software.intel.com / en-us / blogs / 2013/01/06 /… は、それがどれほど悪いかについて驚くほどよく書かれた投稿です。ただし、その特定の投稿はアトミック操作に関するものであり、ほとんどの人にとって非常に紛らわしいので、UBの入門書として、またどのようにうまくいかないかを勧めることは避けます。
コートアンモン

1
Cには、定義済みの値をそのままにして、左辺値またはそれらの配列を初期化されていない非トラップ不定値または未指定値に設定するか、厄介な左辺値をより厄介な値(非トラップ不定または未指定)に変える組み込み関数がCにあったことを望みます。コンパイラーはこのようなディレクティブを使用して有用な最適化を支援し、プログラマーはそれらを使用して、スパースマトリックステクニックのようなものを使用するときに「最適化」を壊すのをブロックしながら、無駄なコードを書く必要を回避できます。
supercat

@supercat有効なソリューションであるプラットフォームをターゲットにしている場合、それは素晴らしい機能です。既知の問題の例の1つは、メモリタイプに対して無効であるだけでなく、通常の手段では達成できないメモリパターンを作成する機能です。 boolは明らかな問題がある優れた例ですが、これらの問題のすべてがオペコード時に解決されるx86、ARM、MIPSなどの非常に役立つプラットフォームで作業していると思わない限り、他の場所に現れます。
コートアンモン

整数演算のサイズにより、オプティマイザーがaに使用される値が8未満であることを証明できる場合を考えてみましょうswitch。したがって、「大きな」値が入るリスクがないと推定される高速命令を使用できます。不特定の値(コンパイラのルールを使用して構築することはできません)が表示され、予期しない何かが実行され、突然、ジャンプテーブルの端から大規模なジャンプが発生します。ここで不特定の結果を許可するということは、プログラム内のすべての switchステートメントに、「発生しない」これらのケースをサポートするための追加のトラップが必要であることを意味します。
コートアンモン

組み込み関数が標準化されている場合、コンパイラはセマンティクスを尊重するために必要なことをすべて実行する必要があります。たとえば、一部のコードパスが変数を設定し、一部が設定せず、組み込み関数が「初期化されていないか不定の場合は指定されていない値に変換し、そうでなければそのままにする」と言う場合、コードを挿入して、変数をコードパスの前に初期化するか、コードパスで初期化しないと初期化されない場合がありますが、そのために必要なセマンティック分析は非常に簡単です。
supercat

5

私はほとんどの場合、変数の再割り当てが許可されていない関数型プログラミング言語で働いています。今まで。これにより、この種のバグが完全に排除されます。これは最初は大きな制限のように見えましたが、新しいデータを学習する順序と一貫した方法でコードを構造化することを余儀なくされます。

これらの習慣は、命令型言語にも引き継がれます。コードをリファクタリングして、変数をダミー値で初期化しないようにすることはほぼ常に可能です。それは、それらのガイドラインがあなたにそうするように言っていることです。彼らは、自動化されたツールを幸せにするだけの何かではなく、そこに意味のある何かを入れてほしいと思っています。

CスタイルAPIを使用した例はもう少し複雑です。そのような場合、関数を使用するとき、コンパイラーが文句を言うのを防ぐためにゼロに初期化しますが、my_readユニットテストで一度、エラー条件が適切に機能することを確認するために別のものに初期化します。使用するたびに考えられるすべてのエラー状態をテストする必要はありません。


5

いいえ、バグを隠しません。代わりに、ユーザーがエラーに遭遇した場合に開発者がエラーを再現できるように、動作を決定論的にします。


1
そして、-1で初期化することは実際には意味があります。「int bytes_read = 0」が悪い場合は、実際に0バイトを読み取ることができるため、-1で初期化すると、バイトの読み取りが成功しなかったことを非常に明確にし、それをテストできます。
ピーターB

4

TL; DR:このプログラムを正しく行うには、変数を初期化して祈るという2つの方法があります。一貫して結果を提供するのは1つだけです。


あなたの質問に答える前に、まず未定義の振る舞いの意味を説明する必要があります。実際には、コンパイラの作者に大部分の作業をさせます。

これらの記事を読みたくない場合、TL; DRは次のとおりです。

未定義の動作は、開発者とコンパイラ間の社会的契約です。コンパイラは、ユーザーが未定義の動作に決して依存しないことを盲目的に想定しています。

「鼻から飛んでいる悪魔」の原型は、残念ながらこの事実の意味を伝えることに完全に失敗しました。何でも起こり得ることを証明することを意図していましたが、それは全く信じられないほどで、ほとんど肩をすくめられました。

しかし、真実は、未定義の動作コンパイルに影響するということです、プログラムを使用しようとするはるか前(インストルメントされているかどうか、デバッガー内かどうか)自体にその動作を完全に変更する可能性があります。

上記のパート2の例が印象的です。

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

に変換されます:

void contains_null_check(int *P) {
  *P = 4;
}

なぜなら、それはチェックされる前に逆参照されているからでPはないのは明らかだ0からです。


これはあなたの例にどのように当てはまりますか?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

さて、あなたは未定義の振る舞いがランタイムエラーを引き起こすと仮定するというよくある間違いを犯しました。そうではないかもしれません。

の定義my_readは次のとおりだと想像しましょう。

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

そして、インライン化を備えた優れたコンパイラの期待どおりに続行します。

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

次に、優れたコンパイラの期待どおり、無駄なブランチを最適化します。

  1. 初期化せずに変数を使用しないでください
  2. bytes_readそうでない場合、初期化さresultれずに使用されます0
  3. 開発者は、それresultは決してないことを約束しています0

result決してそうではありません0

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

ああ、result決して使用されません:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

ああ、次の宣言を延期することができますbytes_read

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

そして、ここに、元の変換を厳密に確認する変換があります。初期化されていない変数がないため、デバッガーはトラップしません。

私はその道を進んでいますが、予想される動作とアセンブリが一致しない場合の問題を理解するのは本当に面白くないです。


コンパイラは、UBパスを実行するときに、ソースファイルを削除するプログラムを取得する必要があると思うことがあります。プログラマは、その後....自分のエンドユーザーにどのようなUB手段を学びます
mattnz

1

サンプルコードを詳しく見てみましょう。

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

これは良い例です。このようなエラーが予想される場合は、行assert(bytes_read > 0);を挿入して実行時にこのバグをキャッチできますが、初期化されていない変数では不可能です。

しかし、そうではなく、関数内でエラーを見つけたとしますuse(buffer)。プログラムをデバッガーにロードし、バックトレースを確認し、このコードから呼び出されたことを確認します。したがって、このスニペットの先頭にブレークポイントを配置し、再度実行して、バグを再現します。私たちはそれをキャッチしようとシングルステップします。

初期化していない場合はbytes_read、ガベージが含まれています。毎回同じガベージが含まれているとは限りません。私たちは境界線を通り過ぎmy_read(buffer, &bytes_read);ます。これで、以前とは異なる値の場合、バグをまったく再現できない可能性があります!次回、同じ入力で、完全に偶然に動作する可能性があります。常にゼロの場合、一貫した動作が得られます。

おそらく同じ実行のバックトレースでも値をチェックします。ゼロの場合、何かが間違っていることがわかります。bytes_read成功した場合はゼロであってはなりません。(または、可能であれば、-1に初期化することもできます。)おそらくここでバグをキャッチできます。bytes_readしかし、もっともらしい値である場合、それがたまたま間違っている場合、一目でそれを見つけることができますか?

これは特にポインターに当てはまります。デバッガーではNULLポインターは常に明白であり、非常に簡単にテストでき、逆参照しようとすると最新のハードウェアでセグメンテーション違反が発生します。ガベージポインターは、後で再現不可能なメモリ破損のバグを引き起こす可能性があり、デバッグするのはほとんど不可能です。


1

OPは未定義の動作に依存していないか、少なくとも正確に依存していません。実際、未定義の動作に依存するのは悪いことです。同時に、予期しない場合のプログラムの動作未定義ですが、別の種類の未定義です。あなたはゼロに変数を設定していますが、用途初期ゼロということは、プログラムの振る舞いがsanelyバグを持っているとするときなると実行パスを持っているつもりはなかった場合に行うようなパスを持っていますか?あなたは今雑草の中にいます。その値を使用する予定はありませんでしたが、とにかく使用しています。無害であったり、プログラムがクラッシュしたり、プログラムが静かにデータを破損したりする可能性があります。分からない。

OPが言っているのは、あなたがそれらを許可した場合、このバグを見つけるのに役立つツールがあるということです。値を初期化せずに、とにかく使用する場合、バグがあることを通知する静的および動的アナライザーがあります。静的アナライザーは、プログラムのテストを開始する前に通知します。一方、値を盲目的に初期化すると、アナライザはその初期値を使用する予定がないことを認識できないため、バグは検出されません。運がよければ、それは無害であるか、単にプログラムをクラッシュさせるだけです。運が悪ければ、静かにデータを破壊します。

私がOPに同意しない唯一の場所は、最後にあります。彼は「そうでなければ、すでにセグメンテーションフォールトを取得するであろう」と言います。実際、初期化されていない変数は確実にセグメンテーションフォールトを生成しません。その代わりに、プログラムを実行しようとすることさえできない静的解析ツールを使用すべきだと思います。


0

質問への回答は、プログラム内に現れるさまざまなタイプの変数に分解する必要があります。


ローカル変数

通常、宣言は、変数が最初に値を取得する場所で行う必要があります。古いスタイルCのように変数を事前宣言しないでください。

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

これにより、99%の初期化が不要になり、変数には最終値がすぐに設定されます。いくつかの例外は、初期化が何らかの条件に依存する場合です。

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

これらのケースを次のように書くのは良い考えだと思います。

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

I. e。変数の適切な初期化が実行されることを明示的にアサートします。


メンバー変数

ここで、他の回答者が言ったことに同意します。これらは、コンストラクタ/初期化子リストによって常に初期化されるべきです。それ以外の場合は、メンバー間の一貫性を確保することは困難です。また、すべての場合で初期化を必要としないと思われるメンバーのセットがある場合は、クラスをリファクタリングし、それらのメンバーを常に必要な派生クラスに追加します。


バッファ

これは私が他の答えに反対するところです。人々が変数の初期化について信心深くなると、しばしば次のようなバッファーの初期化になります。

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

これはほとんど常に有害であると考えていますvalgrind。これらの初期化の唯一の効果は、無力なツールをレンダリングすることです。初期化されたバッファから必要以上に多くのコードを読み取るコードは、おそらくバグです。しかし、初期化では、そのバグはで公開できませんvalgrind。したがって、ゼロで満たされているメモリに本当に依存していない限り、それらを使用しないでください(その場合は、ゼロが必要なものを示すコメントをドロップしてください)。

また、テストスイート全体valgrindまたは同様のツールを実行するビルドシステムにターゲットを追加して、初期化前に使用するバグとメモリリークを公開することを強くお勧めします。これは、変数のすべての事前初期化よりも価値があります。このvalgrindターゲットは定期的に実行する必要があります。最も重要なのは、コードが公開される前です。


グローバル変数

初期化されていないグローバル変数を持つことはできません(少なくともC / C ++などでは)。したがって、この初期化が必要なものであることを確認してください。


、あなたは三項演算子で、条件初期化を書くことができることを確認して例えば Base& b = foo() ? new Derived1 : new Derived2;
Davislor

@Loreheadこれは単純なケースでは機能するかもしれませんが、より複雑なケースでは機能しません:3つ以上のケースがあり、コンストラクタが読みやすさのために3つ以上の引数を取る場合、これを行いたくない理由。また、ループ内の初期化の1つのブランチの引数を検索するなど、実行する必要のある計算も考慮していません。
cmaster

より複雑な場合は、ファクトリ関数で初期化コードをラップできますBase &b = base_factory(which);。これは、コードを複数回呼び出す必要がある場合、または結果を定数にすることができる場合に最も役立ちます。
デビスラー

@Loreheadそれは真実であり、必要なロジックが単純でない場合の方法です。それにもかかわらず、初期化経由?:がPITA である間に小さな灰色の領域があり、工場機能がまだ過剰であることを信じています。これらのケースはほとんどありませんが、存在します。
cmaster

-2

適切なコンパイラオプションが設定された適切なC、C ++、またはObjective-Cコンパイラは、値が設定される前に変数が使用されているかどうかをコンパイル時に通知します。これらの言語では、初期化されていない変数の値を使用することは未定義の動作であるため、「使用する前に値を設定する」はヒント、ガイドライン、または優れたプラクティスではないため、100%の要件です。そうしないと、プログラムは完全に壊れます。JavaやSwiftなどの他の言語では、コンパイラーは変数を初期化する前に使用することを許可しません。

「初期化」と「値の設定」には論理的な違いがあります。ドルとユーロの換算レートを求めて、「double rate = 0.0;」と書きたい場合 変数には値が設定されていますが、初期化されていません。ここに保存される0.0は、正しい結果とは何の関係もありません。この状況で、バグのために正しい変換率を保存しない場合、コンパイラーはそれを伝える機会がありません。「ダブルレート」と書いた場合、意味のある変換率を保存することはありません、コンパイラーが教えてくれます。

そのため、初期化されずに使用されているとコンパイラが通知するからといって、変数を初期化しないでください。それはバグを隠しています。本当の問題は、使用すべきではない変数を使用していること、または1つのコードパスで値を設定しなかったことです。問題を修正し、非表示にしないでください。

変数が初期化されずに使用されているとコンパイラが通知する場合があるため、変数を初期化しないでください。繰り返しますが、あなたは問題を隠しています。

使用に近い変数を宣言します。これにより、宣言の時点で意味のある値で初期化できる可能性が向上します。

変数を再利用しないでください。変数を再利用する場合、2番目の目的で変数を使用すると、ほとんどの場合、変数は無効な値に初期化されます。

一部のコンパイラには偽陰性があり、初期化のチェックは停止の問題と同等であるとコメントされています。どちらも実際には無関係です。引用されているコンパイラが、バグが報告されてから10年後に初期化されていない変数の使用を見つけることができない場合、代替コンパイラを探す時が来ました。Javaはこれを2回実装します。コンパイラーに1回、検証器に1回、問題なく。停止の問題を回避する簡単な方法は、使用する前に変数を初期化することではなく、単純で高速なアルゴリズムで確認できる方法で使用する前に変数を初期化することです。


これは表面的には良いように聞こえますが、初期化されていない値の警告の精度に依存しすぎています。これらの取得完全に正しいことは停止問題と等価であり、生産コンパイラはと(すなわち、それらは偽陰性を受けないことができません、彼らが持つべき時に初期化されていない変数を診断します)。たとえば、GCCバグ18501を参照してください。これは10年以上も修正されていない状態です。
zwol

gccについてあなたが言うことはちょうど言われています。残りは無関係です。
gnasher729

gccについては悲しいですが、残りがなぜ関連するのか理解できない場合は、自分自身を教育する必要があります。
zwol
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.