C++で"undefined behaviour"を使用すると、コンパイラが必要なことをほとんど実行できることを知っています。しかし、コードが十分に安全であると思ったので、私は驚きました。
この場合、実際の問題は、特定のコンパイラーを使用する特定のプラットフォームでのみ発生し、最適化が有効になっている場合にのみ発生しました。
問題を再現して最大限に単純化するために、いくつかのことを試しました。以下はSerialize
と呼ばれる関数の抜粋です。boolパラメーターを取り、文字列true
またはfalse
を既存の宛先バッファーにコピーします。
この関数はコードレビューに含まれますか?boolパラメーターが初期化されていない値である場合、実際にクラッシュする可能性があることを伝える方法はありませんか?
// 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 +最適化で実行されると、クラッシュする/クラッシュする可能性があります。
期待される三項演算子boolValue ? "true" : "false"
は私にとって十分に安全であるように見え、「boolValue
にあるゴミの値は何でも構いません。どうにかしてtrueまたはfalseに評価されるからです」
私は コンパイラエクスプローラーの例 をセットアップしました。これは逆アセンブリの問題を示しています。ここでは完全な例です。 注:問題を再現するために、動作することがわかった組み合わせは、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;
}
問題はオプティマイザーが原因で発生します。文字列「true」と「false」の長さが1だけ異なることを推測するのに十分賢明でした。したがって、実際に長さを計算する代わりに、 should技術的には0または1であり、次のようになります。
const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue; // clang clever optimization
これは「賢い」ですが、いわば私の質問は次のとおりです。C++標準では、boolが '0'または '1'の内部数値表現しか持つことができないとコンパイラが想定しています。そして、そのように使用しますか?
または、これは実装定義の場合ですか?その場合、実装はすべてのブールに0または1のみが含まれ、他の値は未定義の動作領域であると想定しましたか?
コンパイラーは、引数として渡されたブール値が有効なブール値(つまり、初期化またはtrue
またはfalse
に変換された値)であると想定することができます。 true
の値は整数1と同じである必要はありません-実際、true
およびfalse
のさまざまな表現がありますが、パラメーターはこれらの2つの値のいずれかの有効な表現。「有効な表現」は実装定義です。
したがって、bool
の初期化に失敗した場合、または異なる型のポインターを介して上書きに成功した場合、コンパイラーの仮定は間違っており、未定義の動作が発生します。あなたは警告されていました:
50)初期化されていない自動オブジェクトの値を調べるなど、この国際標準で「未定義」と説明されている方法でブール値を使用すると、真でも偽でもないかのように動作する可能性があります。 (§6.9.1、基本型のパラ6の脚注)
関数自体は正しいですが、テストプログラムでは、関数を呼び出すステートメントは、初期化されていない変数の値を使用して未定義の動作を引き起こします。
バグは呼び出し関数にあり、コードレビューまたは呼び出し関数の静的分析によって検出できます。コンパイラエクスプローラリンクを使用して、gcc 8.2コンパイラはバグを検出します。 (clangに対して、問題が見つからないというバグレポートを提出することもできます)。
未定義の動作とは、anythingが発生する可能性があることを意味します。これには、未定義の動作をトリガーしたイベントの後に数行クラッシュするプログラムが含まれます。
NB。 「未定義の動作により_____が発生する可能性がありますか?」に対する回答常に「はい」です。それは文字通り未定義の振る舞いの定義です。
Boolは、true
およびfalse
に内部的に使用される実装依存の値を保持することのみが許可されており、生成されたコードは、これら2つの値のいずれかのみを保持すると想定できます。
通常、実装では、false
の整数0
とtrue
の1
を使用して、bool
とint
の間の変換を簡素化し、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
配列の境界外にアクセスすることになります。
あなたの質問をたくさん要約すると、あなたは尋ねていますC++標準は、コンパイラがbool
が「0」または「1」の内部数値表現しか持つことができないと仮定し、それをそのように使用することを許可しますか?
標準では、bool
の内部表現については何も言及されていません。 bool
をint
にキャストするとき(またはその逆)に何が起こるかを定義するだけです。ほとんどの場合、これらの整数変換(および人々がそれらにかなり依存しているという事実)のため、コンパイラは0と1を使用しますが、必要はありません(ただし、使用する下位レベルのABIの制約を尊重する必要があります) )。
そのため、コンパイラは、bool
が「bool
」または「true
」ビットパターンのいずれかを含むと判断した場合、false
を認識する権利があります。そして、それが好きなことをしてください。したがって、true
とfalse
の値がそれぞれ1と0の場合、コンパイラーはstrlen
を5 - <boolean value>
に最適化することが実際に許可されます。他の楽しい行動が可能です!
ここで繰り返し述べられるように、未定義の動作には未定義の結果があります。含むがこれらに限定されません
すべてのプログラマーが未定義の動作について知っておくべきこと を参照してください