C ++標準では、初期化されていないブールがプログラムをクラッシュさせることを許可していますか?


500

C ++の「未定義の動作」により、コンパイラーが必要なことをほとんど実行できることがわかっています。しかし、コードが十分に安全であると思っていたので、驚いたクラッシュがありました。

この場合、実際の問題は、特定のコンパイラを使用する特定のプラットフォームでのみ、最適化が有効になっている場合にのみ発生しました。

問題を再現し、それを最大限に簡略化するために、いくつかのことを試みました。Serializeこれはと呼ばれる関数の抜粋です。これはboolパラメータを取り、文字列trueまたはfalse既存の宛先バッファにコピーします。

この関数はコードレビューに含まれますか?実際には、ブールパラメーターが初期化されていない値である場合にクラッシュする可能性があることを伝える方法はありませんか?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

このコードをclang 5.0.0 +最適化で実行すると、クラッシュする可能性があります。

予想される3項演算子boolValue ? "true" : "false"は私にとっては十分安全に見えました。「ガベージ値が何であれboolValue、trueとfalseのどちらに評価されるかは問題ではない」と私は思っていました。

分解の問題を示すCompiler Explorerの例をセットアップしました。ここでは完全な例を示します。注:問題を再現するために、Clang 5.0.0と-O2最適化を組み合わせて使用​​すると、うまくいったことがわかりました。

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

問題は、オプティマイザの起こる:それは「真」と「偽」のみ1だからではなく、本当に長さを計算することにより、長さが異なる文字列は、それがどの、ブール自体の値を使用していることを推測する賢い十分だったはず技術的には0または1であり、次のようになります。

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

これは「賢い」ですが、いわば私の質問です。C++標準では、コンパイラはブールが「0」または「1」の内部数値表現しか持てないと想定して、そのような方法で使用することを許可していますか?

または、これは実装定義のケースですか?その場合、実装はすべてのブール値が0または1のみを含むと想定し、他の値は未定義の動作領域です。


200
それは素晴らしい質問です。これは、未定義の動作が単なる理論的な問題ではないことを示しています。UBの結果として何かが起こる可能性があると人々が言うとき、その「何か」は本当に驚くべきことです。未定義の動作が依然として予測可能な方法で現れていると考える人もいるかもしれませんが、最近では現代のオプティマイザーではまったく真実ではありません。OPは、MCVEの作成に時間をかけ、問題を徹底的に調査し、分解を検査し、それについて明確で簡単な質問をしました。これ以上は要求できませんでした。
John Kugelman、

7
「ゼロ以外のtrue値がに評価される」という要件は、「ブール値への割り当て」(static_cast<bool>()詳細に応じて暗黙的にを呼び出す可能性がある)を含むブール演算に関するルールであることに注意してください。ただしbool、コンパイラが選択したの内部表現に関する要件ではありません。
Euro Micelli、

2
コメントは拡張ディスカッション用ではありません。この会話はチャットに移動しました
Samuel Liew

3
非常に関連する注意として、これはバイナリ非互換性の「楽しい」ソースです。関数を呼び出す前に値をゼロで埋めるABI Aがあるが、パラメーターがゼロで埋められると想定するように関数をコンパイルし、反対のABI B(ゼロを埋めないがゼロは想定しない)の場合-paddedパラメータ)、それはほとんど機能しますが、B ABIを使用する関数は、「小さな」パラメータを取るA ABIを使用する関数を呼び出すと問題を引き起こします。IIRCあなたはclangとICCを備えたx86でこれを持っています。
TLW

1
@TLW:標準は、実装が外部コードによって呼び出しまたは呼び出される手段を提供することを要求していませんが、関連する実装(そのような詳細が実装されていない実装)にそのようなものを指定する手段があると役立つでしょう関連はそのような属性を無視できます)。
スーパーキャット

回答:


285

はい。ISOC ++では、この選択を実装に許可しています(ただし必須ではありません)。

ただし、ISO C ++では、プログラムがUBに遭遇した場合(たとえば、エラーの検出を支援する方法として)、コンパイラーは意図的に(たとえば、不正な命令によって)クラッシュするコードを発行することもできます。(またはそれがDeathStation 9000であるため。厳密に準拠しているだけでは、C ++実装が実際の目的に役立つには不十分です)。 したがって、ISO C ++を使用すると、初期化されていないを読み取る同様のコードでも、コンパイラーは(まったく異なる理由で)クラッシュしたasmを作成できuint32_tます。 これは、トラップ表現のない固定レイアウトタイプである必要があります。

実際の実装がどのように機能するかについては興味深い質問ですが、答えが異なっていても、最新のC ++はアセンブリ言語の移植可能なバージョンではないため、コードは安全ではありません。


x86-64 System V ABI用にコンパイルしています。これboolは、レジスタの関数引数としてのaがビットパターンfalse=0true=1レジスターの下位8ビットで 1。メモリ内でboolは、1バイト型であり、やはり0または1の整数値が必要です。

(ABIは、同じプラットフォームのコンパイラが同意する実装の選択肢のセットであり、タイプサイズ、構造体レイアウトルール、呼び出し規則など、互いの関数を呼び出すコードを作成できます。)

ISO C ++はそれを指定していませんが、このABIの決定は、bool-> int変換を安価に(単にゼロ拡張)するために広く行われていますboolどのアーキテクチャでも(x86だけでなく)、コンパイラーに0または1を想定させないABIについては知りません。下位ビットをフリップするような!myboolwith 最適化を可能にしますxor eax,1単一のCPU命令でビット/整数/ブールを0と1の間でフリップできるあらゆるコード。またはa&&bbool型のビットごとのANDにコンパイルします。一部のコンパイラは、実際にはブール値をコンパイラの8ビットとして利用します。それらの操作は非効率的ですか?

一般に、as-ifルールにより、コンパイラーは、コンパイル対象のターゲットプラットフォームで trueであるものを利用できます。これは、C ++ソースと同じ外部から見える動作を実装する実行可能コードになるためです。(未定義の動作が実際に「外部から見える」ものに課しているすべての制限付き:デバッガではなく、整形式の/正当なC ++プログラムの別のスレッドから。)

コンパイラーは、そのコード生成でABI保証を最大限に活用し、最適化さstrlen(whichString)れたコードを作成することができます
5U - boolValue
(ちなみに、この最適化は一種の巧妙な方法ですが、多分、近視眼的対memcpy、即時データのストアとしての分岐とインライン化2です。)

または、コンパイラがポインタのテーブルを作成し、それをbool0か1であると仮定して、の整数値でインデックスを付けた可能性もあります(この可能性は@Barmarの答えが示唆したものです)です)。


あなたの__attribute((noinline))最適化とコンストラクタは、同じように使用することにスタックからバイトを読み込む打ち鳴らすにつながっ有効にuninitializedBool。これにより、オブジェクトのスペースが作成されましmainpush rax(これはサイズが小さく、さまざまな理由でと同じくらい効率的ですsub rsp, 8)。そのため、入り口でALにあったガベージは、mainそれが使用した値になりuninitializedBoolます。これが実際にだけではない値を得た理由です0

5U - random garbage大きな符号なしの値に簡単にラップでき、memcpyがマップされていないメモリに入るようにします。宛先はスタックではなく静的ストレージにあるため、戻りアドレスなどを上書きすることはありません。


他の実装では、false=0となど、異なる選択を行うことができtrue=any non-zero valueます。その場合、clangはおそらく、この UBの特定のインスタンスでクラッシュするコードを作成しません。(ただし、必要に応じて許可されます。) x86-64が何をするかを選択する実装については知りませんboolが、C ++標準では、誰も実行しない、または実行したいことを多く許可しています。現在のCPUのようなハードウェア。

ISO C ++では、のオブジェクト表現を調べたり変更したりしたときに何が見つかるかは不明ですbool。(たとえばmemcpyboolinto を使用すると、何でもエイリアスできるunsigned charため、許可されますchar*unsigned charパディングビットがないことが保証されているため、C ++標準では、UBなしでオブジェクト表現を正式に16進ダンプできます。オブジェクトをコピーするポインタキャストchar foo = my_boolもちろん、表現はの割り当てとは異なるため、0または1へのブール化は行われず、生のオブジェクト表現が取得されます。)

を使用して、コンパイラからこの実行パス上のUBを部分的に「非表示」にしましたnoinline。ただし、インライン化されていなくても、プロシージャ間の最適化によって、別の関数の定義に依存する関数のバージョンが作成される可能性があります。(1つ目は、clangが実行可能ファイルを作成し、シンボル挿入が発生する可能性のあるUnix共有ライブラリではありません。2つ目は、定義内のclass{}定義なので、すべての翻訳単位が同じ定義を持つ必要があります。inlineキーワードです。)

したがって、コンパイラは、の定義としてretor ud2(不正な命令)だけを出力する可能性がmainあります。これは、先頭から始まる実行パスがmain未定義の動作に遭遇するためです。(非インラインコンストラクターを介してパスをたどることにした場合、コンパイラーはコンパイル時に確認できます。)

UBに遭遇するプログラムは、その存在が完全に定義されていません。しかし、if()実際には実行されない関数またはブランチ内のUB は、プログラムの残りの部分を破損しません。実際には、コンパイラはret、コンパイル時にUBを含むか、またはUBにつながることを証明できる基本ブロック全体について、不正な命令を発行するか、何も発行しないか、または何も発行せずに次のブロック/関数に分類されることを決定できます。

実際には、GCCとClangは実際にud2 UBでエミットすることがありますが、意味のない実行パスのコードを生成しようとすることさえありません。 あるいは、非void関数の終わりから落ちるような場合、gccは時々ret命令を省略します。「私の機能はRAXにあるゴミで何でも返す」と思っていたら、あなたはひどく間違っています。 最近のC ++コンパイラは、この言語をポータブルアセンブリ言語のように扱いません。プログラムは、スタンドアロンのインライン化されていないバージョンの関数がasmでどのように見えるかを想定せずに、実際に有効なC ++である必要があります。

別の楽しい例は、なぜmmapされたメモリへの非境界整列アクセスがAMD64でsegfaultになることがあるのですか?。x86は、境界整列されていない整数でエラーになりませんか?では、なぜずれuint16_t*が問題になるのでしょうか?なぜならalignof(uint16_t) == 2SSE2で自動ベクトル化すると、その仮定に違反してセグメンテーション違反が発生したです。

未定義の動作#1/3についてすべてのCプログラマが知っておくべきことも参照してください。clang開発者による記事である、。

重要なポイント:コンパイラーがコンパイル時にUBに気づいた場合、ビットパターンがの有効なオブジェクト表現であるABIをターゲットにしている場合でも、UBの原因となるコードのパス「壊れる」(驚くべきasmが出力される)可能性がありますbool

プログラマーによる多くの間違い、特に最新のコンパイラーが警告するものに対する完全な敵意を期待してください。このため-Wall、警告を使用して修正する必要があります。C ++はユーザーフレンドリーな言語ではありません。C++の何かは、コンパイルするターゲットのasmで安全であっても安全ではない場合があります。(たとえば、符号付きオーバーフローはC ++ではUBであり、コンパイラーは、2の補数x86を使用してコンパイルする場合でも、を使用しない限り、発生しないと想定しますclang/gcc -fwrapv。)

コンパイル時に表示されるUBは常に危険であり、(リンク時の最適化によって)UBをコンパイラーから本当に隠していることを確認することは非常に難しいため、どのような種類のasmが生成されるのかを推測できます。

過度に劇的ではありません。多くの場合、コンパイラーはいくつかのことを回避して、何かがUBであっても期待しているようなコードを発行します。しかし、コンパイラの開発者が値の範囲についてより多くの情報を得るいくつかの最適化を実装する場合(たとえば、変数が負ではなく、x86でゼロ拡張を解放するために符号拡張を最適化できるようにする場合) 64)。たとえば、現在のgccとclangでは、always-falseとしてtmp = a+INT_MIN最適化するのではなくa<0tmp常に負になるだけです。(INT_MIN+ a=INT_MAXはこの2の補数ターゲットで負なので、aあり、それより高くすることはできないためです。)

そのため、gcc / clangは現在、計算の入力の範囲情報を導出するためにバックトラックせず、符号付きオーバーフローがないという仮定に基づく結果にのみ基づいています:Godboltの例。これが最適化であるかどうかは、ユーザーフレンドリーという意味で意図的に「見落とされている」かどうかはわかりません。

また、実装(別名コンパイラー)は、ISO C ++がundefinedのままにする動作を定義できることに注意してください。たとえば、インテルの組み込み関数(_mm_add_ps(__m128, __m128)手動のSIMDベクトル化など)をサポートするすべてのコンパイラーは、誤って位置合わせされたポインターの形成を許可する必要があります。これは、逆参照しなくても、C ++ではUBです。 またはではなく、__m128i _mm_loadu_si128(const __m128i *)正しく整列されていない__m128i*引数を取得することにより、整列されていないロードを行います。 ハードウェアのベクトルポインターと対応する型の間の `reinterpret_cast`は未定義の動作ですか?void*char*

GNU C / C ++はまた-fwrapv、通常のsigned-overflow UBルールとは別に、負の符号付き数値を(なしでも)左シフトする動作を定義します。(これはISO C ++のUBですが、符号付き数値の右シフトは実装で定義されます(論理と算術)。高品質の実装では、算術右シフトがあるHWで算術を選択しますが、ISO C ++では指定しません)。これは、GCCマニュアルのIntegerセクションに記載されているほか、C標準では実装が何らかの方法で定義する必要がある実装定義の動作を定義しています。

コンパイラの開発者が気にする実装品質の問題は確かにあります。彼らは通常、意図的に敵対的なコンパイラを作成しようとはしていませんが、C ++のすべてのUBポットホール(定義することを選択したものを除く)を利用して最適化することは、ほとんど区別がつかない場合があります。


脚注1:レジスターよりも狭い型の場合、通常、上位56ビットは、呼び出し先が無視しなければならないゴミである可能性があります。

他のABI ここで異なる選択をします。MIPS64やPowerPC64などの関数に渡されるとき、または関数から返されるときにレジスタを満たすために、ゼロまたは符号拡張される狭い整数型を必要とするものもありますこのx86-64回答の最後のセクションを参照してください。これは、以前のISAと比較したものです。)

たとえば、呼び出し元はa & 0x01010101RDIで計算し、を呼び出す前に別の目的で使用した可能性がありますbool_func(a&1)。呼び出し側は&1、の一部としてすでに下位バイトにand edi, 0x01010101それを行っており、呼び出し先は上位バイトを無視する必要があることを知っているので、最適化することができます。

または、ブール値が3番目の引数として渡された場合、コードサイズを最適化する呼び出し元mov dl, [mem]movzx edx, [mem]、の代わりにそれをロードし、RDXの古い値への誤った依存関係(またはその他の部分レジスター効果、 CPUモデル)。または、いずれにしてもREXプレフィックスが必要なため、のmov dil, byte [r10]代わりに最初の引数を使用しmovzx edi, byte [r10]ます。

これがclangが発行する理由です movzx eax, dilSerialize、代わりにsub eax, edi。(整数の引数の場合、clangはこのABIルールに違反します。代わりに、gccおよびclangのドキュメント化されていない動作に基づいて、32ビットに狭い整数をゼロ拡張または符号拡張します。32ビットの オフセットをポインターに追加するときに、符号またはゼロ拡張が必要ですか? x86-64 ABI? それで私はそれがに対して同じことをしないことを見て興味がありましたbool


脚注2: 分岐後は、4バイトのmovイミディエイトストア、または4バイト+ 1バイトのストアになります。長さは、ストアの幅+オフセットでは暗黙的です。

OTOH、glibc memcpyは、長さに依存するオーバーラップで2つの4バイトのロード/ストアを実行するため、これにより、ブール値の条件付きブランチがまったくなくなります。glibcのmemcpy / memmoveのL(between_4_7):ブロックを参照してください。または、少なくとも、memcpyのブランチのどちらのブール値でも同じ方法でチャンクサイズを選択します。

インライン化する場合は、2x mov-immediate + cmovと条件付きオフセットを使用するか、文字列データをメモリに残すことができます。

または、インテルアイスレイクのチューニング(Fast Short REP MOV機能を使用)の場合、実際rep movsbは最適な場合があります。glibcmemcpyrep movsb 、その機能を備えたCPUで小さなサイズの使用を開始し、多くの分岐を節約できます。


UBおよび初期化されていない値の使用を検出するためのツール

gccとclangでは、次のコマンドでコンパイルできます。 -fsanitize=undefinedて、実行時に発生するUBで警告またはエラーになるランタイムインストルメンテーションを追加できます。ただし、これはユニタライズされた変数をキャッチしません。(「初期化されていない」ビットのための余地を作るために型のサイズを増やしないため)。

https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/を参照してください

初期化されていないデータの使用を見つけるために、clang / LLVMにAddress SanitizerとMemory Sanitizerがあります。 https://github.com/google/sanitizers/wiki/MemorySanitizerは、clang -fsanitize=memory -fPIE -pie初期化されていないメモリ読み取りを検出する例を示しています。生成されたasm 最適化最適にため、変数のすべての読み取りは実際にはasmのメモリからロードされます。時に使用されていることが示さ変更せずにコンパイルすると最適に動作し、結果としてこれがチェックされる可能性があります。)-O2負荷が最適化されない場合ます。私自身は試していません。(たとえば、配列を合計する前にアキュムレーターを初期化しない場合、clang -O3は合計しないベクトルレジスターにコードを出力し、初期化されませんでした。そのため、最適化により、UBに関連付けられたメモリの読み取りがない場合があります。 。 だが-fsanitize=memory

初期化されていないメモリのコピー、およびそれを使用した単純なロジックと算術演算を許容します。一般に、MemorySanitizerはメモリ内の初期化されていないデータの広がりを静かに追跡し、初期化されていない値に応じてコード分岐が行われた(または行われなかった)ときに警告を報告します。

MemorySanitizerは、Valgrind(Memcheckツール)にある機能のサブセットを実装しています。

初期化されていないメモリから計算されたメモリを使用してglibc memcpyを呼び出すlengthと(ライブラリ内で)、に基づく分岐が発生するため、このケースで機能するはずlengthです。cmov、インデックス、および2つのストアのみを使用した完全にブランチのないバージョンをインライン化した場合、機能しなかった可能性があります。

Valgrindmemcheckはこの種の問題も探しますが、プログラムが初期化されていないデータを単にコピーするだけの場合は不満はありません。ただし、「条件付きジャンプまたは移動が初期化されていない値に依存している」ことを検出し、初期化されていないデータに依存する外部から見える動作をキャッチしようとするという。

おそらく、ロードのみにフラグを立てないことの背後にある考え方は、構造体にパディングを含めることができ、広いベクトルロード/ストアで構造体全体(パディングを含む)をコピーしても、個々のメンバーが一度に1つしか書き込まれなかった場合でもエラーにはなりません。asmレベルでは、パディングされたもの、および実際に値の一部であるものに関する情報は失われました。


2
変数が8ビット整数の範囲ではなく、CPUレジスタ全体の値しか取らないという最悪のケースを見ました。また、Itaniumにはさらに悪い問題があります。初期化されていない変数を使用すると、完全にクラッシュする可能性があります。
Joshua

2
@ジョシュア:ああ、そうですね、Itaniumの明示的な推測は、値を使用しないように、「数値ではない」と同等のタグでレジスタ値をタグ付けします。
Peter Cordes

11
さらに、これはUB featurebugが言語CおよびC ++の設計に最初に導入された理由も示しています。これにより、コンパイラーにまさにこの種の自由が与えられ、最新のコンパイラーがこれらの高品質を実行できるようになりましたC / C ++をそのような高性能中レベル言語にする最適化。
The_Sympathizer

2
そのため、C ++コンパイラの作成者と有用なプログラムを作成しようとするC ++プログラマの間の戦争が続いています。この質問は、この質問への回答において完全に包括的ですが、静的分析ツールのベンダーに説得力のある広告コピーをそのまま使用することもできます...
davidbak

4
@The_Sympathizer:UBは、実装が顧客にとって最も有用な方法で動作できるようにするために含まれていました。すべての動作が同等に有用であると見なされるべきであると示唆することは意図されていませんでした。
スーパーキャット

56

コンパイラーは、引数として渡されたブール値が有効なブール値(つまり、初期化またはに変換されたtrueものfalse)であると想定できます。true値は整数1と同じである必要はない-実際に、そこの様々な表現とすることができるtruefalse-しかし、パラメータが「有効な表現は、」インプリメンテーションで、これら二つの値のいずれかのいくつかの有効な表現でなければなりません定義された。

したがって、の初期化に失敗したbool場合、または別の型のポインタを介してそれを上書きすることに成功した場合は、コンパイラの想定が誤って、未定義の動作が発生します。あなたは警告されていました:

50)初期化されていない自動オブジェクトの値を調べるなど、この国際標準で「未定義」として説明されている方法でブール値を使用すると、trueまたはfalseのように動作しない場合があります。(§6.9.1の第6項の脚注、基本型)


11
true値は整数1と同じである必要はありません」は誤解を招くようなものです。確かに、実際のビットパターンは、可能性が何か他のものであってもよいが、暗黙的に変換したときに/昇格(あなたが以外の値を参照したい唯一の方法true/はfalse)、true常に1、そしてfalse常にあります0。もちろん、そのようなコンパイラーは、このコンパイラーが使用しようとしていたトリックを使用することもできません(bool実際のビットパターンは0またはのみであるという事実を使用して1)ので、それは一種のOPの問題とは無関係です。
ShadowRanger

4
@ShadowRangerオブジェクト表現をいつでも直接検査できます。
TC、

7
@shadowranger:私のポイントは、実装が担当しているということです。の有効な表現をtrueビットパターンに制限する場合、それは1その特権です。他の表現セットを選択した場合、実際にここで説明した最適化を使用できません。その特定の表現を選択する場合は、選択できます。内部的に一貫している必要があるだけです。バイト配列にコピーすることにより、の表現を調べることができboolます。それはUBではありません(ただし、実装によって定義されます)
rici

3
はい、最適化コンパイラー(つまり、実際のC ++実装)boolは、0またはのビットパターンに依存するコードを出力することがあります1。それらはbool、メモリ(または関数の引数を保持するレジスタ)から読み取るたびにブール値を再生成しません。それがこの答えが言っていることです。 :gcc4.7 +を最適化することができるreturn a||bor eax, edi返す関数でbool、またはMSVCを最適化することができるa&bまでtest cl, dl。x86 testビット単位な andので、if cl=1dl=2testはに従ってフラグを設定しcl&dl = 0ます。
Peter Cordes

5
未定義の動作に関する要点は、コンパイラーがそれについてはるかに多くの結論を出すことが許可されていることです。たとえば、初期化されていない値へのアクセスにつながるコードパスがまったく取られないと仮定することは、それがプログラマーの責任であることを保証するためです。 。つまり、低レベルの値がゼロまたは1と異なる可能性だけではありません。
Holger

52

関数自体は正しいですが、テストプログラムでは、関数を呼び出すステートメントが、初期化されていない変数の値を使用して未定義の動作を引き起こします。

バグは呼び出し元の関数にあり、呼び出し元の関数のコードレビューまたは静的分析によって検出できます。コンパイラエクスプローラリンクを使用して、gcc 8.2コンパイラはバグを検出します。(多分あなたはそれが問題を見つけられないというclangに対してバグ報告を提出することができたでしょう)。

未定義の動作とは、何かが発生する可能性があることを意味ます。これには、未定義の動作をトリガーしたイベントの後にプログラムが数行クラッシュすることが含まれます。

NB。「未定義の動作は_____を引き起こす可能性がありますか?」に対する答え 常に「はい」です。それが文字通り未定義の振る舞いの定義です。


2
最初の条項は正しいですか?初期化されていないトリガーUBをコピーするだけboolですか?
ジョシュアグリーン

10
@JoshuaGreenは[dcl.init] / 12を参照してください。「評価によって不確定な値が生成された場合、次の場合を除いて、動作は未定義boolです。」コピーにはソースの評価が必要
MM

8
@JoshuaGreenそしてその理由は、いくつかのタイプのいくつかの無効な値にアクセスすると、ハードウェア障害をトリガーするプラットフォームがある可能性があるためです。これらは「トラップ表現」と呼ばれることもあります。
David Schwartz、

7
Itaniumはあいまいですが、まだ生産されているCPUであり、トラップ値があり、少なくとも2つの最新のC ++コンパイラ(Intel / HP)が2つあります。文字通りtruefalsenot-a-thingブール値を持っています。
MSalters

3
反対に、「標準はすべてのコンパイラが何かを特定の方法で処理することを要求しますか」に対する答えは一般的には「いいえ」です。何かがより明白であるほど、標準の作成者が実際にそれを言う必要性が少なくなるはずです。
スーパーキャット

23

BOOLのみのために内部で使用される実装依存の値を保持させるtruefalse、生成したコードは、それが唯一のこれら2つの値のいずれかを保持すると仮定することができます。

通常、実装では0for false1for の整数を使用して、とのtrue間の変換を簡略化boolint、とif (boolvar)同じコードを生成しますif (intvar)。その場合、割り当ての3項に対して生成されたコードが、2つの文字列へのポインターの配列へのインデックスとして値を使用することを想像できます。つまり、次のように変換されます。

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

boolValueが初期化されていない場合、実際には整数値を保持できるため、strings配列の境界外にアクセスする可能性があります。


1
@SidSありがとう。理論的には、内部表現は、整数へのキャスト/整数からのキャストの逆になる可能性がありますが、それは逆です。
Barmar

1
あなたは正しいです、そしてあなたの例もクラッシュします。ただし、初期化されていない変数を配列のインデックスとして使用していることは、コードレビューでは「可視」です。また、デバッグ中でもクラッシュします(たとえば、一部のデバッガー/コンパイラーは特定のパターンで初期化され、クラッシュしたときに見やすくなります)。私の例では、意外なのはブールの使用法が見えないということです。オプティマイザは、ソースコードにない計算でそれを使用することにしました。
Remz

3
@Remz私は配列を使用して、生成されたコードが何に相当するかを示しています。実際に誰かがそれを書くことを示唆しているわけではありません。
Barmar

1
@Remzリキャストboolまでint*(int *)&boolValueし、それ以外のものであるかどうかを確認し、デバッグ目的のためにそれを印刷し0たり1するとき、それがクラッシュ。その場合は、コンパイラーがインライン-ifを配列として最適化し、それがクラッシュする理由を説明しているという理論をほぼ裏付けています。
Havenard

2
@MSalters:std::bitset<8>さまざまなフラグすべてにわかりやすい名前を付けていません。それらが何であるかに応じて、それは重要かもしれません。
Martin Bonnerがモニカをサポートする

15

あなたの質問をまとめると、あなたは次のように質問しています:C ++標準では、コンパイラーboolは内部で「0」または「1」の数値表現しか持つことができないと仮定して、そのような方法で使用できますか?

標準は、の内部表現については何も述べていませんbool。それだけで、鋳造時に何が起こるかを定義boolするint(またはその逆)。ほとんどの場合、これらの整数変換(および人々がそれらにかなり依存しているという事実)のため、コンパイラーは0と1を使用しますが、必ずしもそうする必要はありません(ただし、使用する下位レベルのABIの制約を尊重する必要があります) )。

したがって、コンパイラは、a boolbool' true'または ' false'ビットパターンのいずれかを含んでいると見なし、感じていることを実行する資格があると判断した場合、したがって、trueおよびの値がfalseそれぞれ1および0の場合、コンパイラーは実際にに最適化strlenすることができ5 - <boolean value>ます。他の楽しい行動が可能です!

ここで繰り返し述べられるように、未定義の動作は未定義の結果をもたらします。含むがこれらに限定されません

  • あなたのコードは期待通りに機能しました
  • コードがランダムに失敗する
  • コードがまったく実行されていない。

未定義の動作についてすべてのプログラマが知っておくべきことを参照してください

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