
2025/12/30 3:17
未定義動作への関心を呼び起こした、本番環境で発生したバグ。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
改訂版まとめ
この記事では、C++ の HTTP‑API ハンドラにおける実際のバグを報告しています。
Response 構造体は 2 つの bool メンバーと 1 つの std::string を持ち、デフォルト構築された場合です。この構造体には非 POD(Plain Old Data)メンバーがあるため、コンパイラは各フィールドを デフォルト初期化 するだけのデフォルトコンストラクタを生成します。結果として bool フィールドは未定義状態のままとなり、これらの値を読み取ると { "error": true, "succeeded": true } が返されますが、実際にはエラーは発生していません。
記事では C++ のデフォルト初期化規則について説明し、2 つの修正策を提示しています。
- 明示的に両方の
メンバーをbool
に初期化するデフォルトコンストラクタを追加(文字列はfalse
を使用)。{} - クラス内でデフォルト値を設定 (
)。bool error = false; bool succeeded = false;
簡易的な回避策として、変数を値初期化 (
Response response{};) するとすべてのフィールドがゼロパディングされ、定義を変更せずに問題を解消できます。
また、記事は静的解析ツールについてもレビューしています。Clang/Tidy は構造体が関数へ渡された際にこのパターンを検出でき、cppcheck も過去には検出していましたが、将来の回帰が懸念されています。実行時サニタイザー(ASan/UBSan)は無効な
bool 値 (8) を検出することで未定義動作をフラグします。
同様のケースをビルド時に捕捉するため、著者は libclang プラグインを構築しました。コードベースで 1 件のみ偽陽性が検出されました。記事では
bool の未定義値読取は UB とされていますが、unsigned char や std::byte のような型ではそうではないと指摘しています。
結論として、構造体設計と初期化に細心の注意を払うことで、予測不能な API 応答や下流での失敗につながる微妙な未定義動作を回避するよう促しています。
本文
すべての記事へ戻る
公開日: 2025‑12‑27
目次
- バグレポート
- 調査
- 自己処理に十分なロープ
- 結果
- 静的解析が救う?
- 実行時解析で救われる
- 再びの結果
- あなたへ贈るC++ルール集
- まとめ
- 補足
1. バグレポート
ひとつのHTTPエンドポイントが、成功か失敗を示す小さなJSON‑ishペイロードを返します。
{ "error": false, "succeeded": true }
あるいは
{ "error": true, "succeeded": false }
(実際のフォーマットは form‑encoded だったかもしれませんが、バグ自体はそれとは無関係です)
クライアントからは以下のようなレスポンスを受け取ったと報告されました。
{ "error": true, "succeeded": true }
これはあり得ない状態です。
2. 調査
該当コードはひとつの関数に集約されています。
struct Response { bool error; bool succeeded; std::string data; }; void handle() { Response response; try { // … データベース処理(response には触れない) response.succeeded = true; } catch (...) { response.error = true; } response.write(); }
構造体のフィールドを書き換える場所は二箇所だけです。
それでも両方のブールが
true になることがどうして起こるのでしょうか?
3. 自己処理に十分なロープ
原因は 初期化されていないメンバー にありました。
C++ のデフォルト初期化規則(簡略版)
| 型 | 宣言 | 結果 |
|---|---|---|
プリミティブ (, ) | | 未定義値 (indeterminate) |
| POD / トリアル構造体 | | すべてのフィールドが未定義 |
| 配列 | | 各要素はデフォルト初期化される |
| 非トリビアル構造体(非トリビアルメンバーを持つ) | | デフォルトコンストラクタが呼ばれ、プリミティブメンバーは未定義のまま |
Response は std::string を含むため、コンパイラはデフォルトコンストラクタを生成し data{} を呼びます。しかし
bool メンバーは 未定義 のままで、読み取ると未定義動作(UB)になります。
4. 結果
簡単な修正策
-
明示的にデフォルトコンストラクタを定義
struct Response { bool error; bool succeeded; std::string data; Response() : error(false), succeeded(false), data{} {} }; -
クラス内イニシャライザを使う
struct Response { bool error = false; bool succeeded = false; std::string data; }; -
呼び出し側でゼロ初期化
Response response{}; // すべてのメンバーを値初期化
後者は構造体定義を変更せずに済むため、最も簡単です。
5. 静的解析が救う?
単独ではclang
を付けても警告しません。-Weverything
は検出できることがありますが、従来はその変数を関数に渡したときだけ報告していました。clang-tidy
はバージョンやオプションによって見逃す場合があります。cppcheck
6. 実行時解析で救われる
AddressSanitizer (ASan) を使うと UB が検出されます。
clang++ -std=c++11 -g -fsanitize=address,undefined main.cpp && ./a.out
実行結果では無効な
bool のロードがハイライトされます。Valgrind でも同様に検出可能です。
これらツールは非常に有用ですが、実行時オーバーヘッドが発生し、一部ケースを見逃すことがあります。
7. 再びの結果
ビルド時にこのような問題を捕捉するために、独自の libclang プラグインを作成しました。
コードベースでは他に誤検知は1件だけでした。
8. あなたへ贈るC++ルール集
いくつかの型は 特殊 です:
| 型 | 未初期化時の挙動 |
|---|---|
, , (符号なし) | コピー/代入で UB は起きず、値は未定義のまま読み取れる。 |
| 書き込む前に読めば UB。ASan で即座に検出されます。 |
例:
unsigned char c; // 未定義 unsigned char d = c; // OK、d は未定義のまま assert(c == d); // 成り立つが両方未定義 bool e; bool f = e; // UB – ASan がフラグを立てる
9. まとめ
- C で無害だった構文も、C++ では危険になることがあります。
- デフォルト初期化規則は微妙でバージョン依存です。
- 未定義動作はプログラム状態を暗黙に破壊し、信頼性を損ないます。
- 静的ツールは限界があり、実行時サニタイザーがほとんどの問題を検出しますが、性能への影響があります。
未初期化の
bool が本番環境で実際にバグを引き起こしました。これらのニュアンスを理解し、安全な C++ 開発を行うことが不可欠です。
10. 補足
フォレスト・ガンプ GIF を忘れていたので、コメントしてくださった方に感謝します!