HN 日本語サマリー

← 一覧へ戻る
プログラミング

C++26: 自明な無限ループはもはや未定義の動作ではなくなる

C++26: Trivial infinite loops are no longer undefined behaviour (sandordargo.com)

154 pointsby ibobev212 コメント

要約

C++26では、`while(true);`のような自明な無限ループが未定義の動作ではなくなり、仕様として定義されるようになります。これにより、コンパイラがこれらのループを最適化で削除してしまう問題が解消され、組み込みシステムやセキュリティ分野での予期せぬ動作を防ぐことができます。この変更は、ループ本体が空で、条件式が常に真である定数式である場合に適用され、代わりに`std::this_thread::yield()`が呼び出されるようになります。

全文翻訳

C++26: 自明な無限ループはもはや未定義動作ではなくなる

まず、質問です!このプログラムは定義されていますか?

int main() {
    while (true)
        ;
}

もし「はい」と答えたなら、あなたは間違っています — 少なくともC++26より前は。副作用のないwhile (true);ループは、かつて未定義動作でした。コンパイラはそれを終了すると仮定することができ、一部のコンパイラ(特にClang)はそれを完全に最適化して削除してしまい、驚くべき結果を招いていました。

// https://godbolt.org/z/WYMxxeW1T
#include <iostream>

int main() {
    while (true)
        ;
}

void unreachable() {
    std::cout << "Hello world!" << std::endl;
}

Clangでは、これは「Hello world!」と出力します。コンパイラは無限ループを削除し、mainはフォールスルーして、リンカが配置したunreachable()関数が実行されます。これはコンパイラのバグではありません — 単なるUB(未定義動作)であり、鼻の悪魔よりはマシです。

最近、私はC++26が未定義動作をどのように削減するかについて書きましたが、この件は完全に忘れっていました。これは、C++26の新機能に関するCppConの講演を準備しているときに気づいたのです — というわけで、今ここで説明します。

C++26はP2809R3によってこれを修正します。自明な無限ループは、定義された動作になります。言及された提案は欠陥報告としても受理されたため、実装は以前のC++モードにもこの修正を適用する可能性があります。そのため、最新のコンパイラではC++20モードであっても古い動作を再現できない場合があります。

どうしてこうなったのか?

この話は、C++11でスレッディングサポートと同時に導入されたフォワードプログレス保証から始まります。標準では([intro.progress])、実装は、任意の Сレッドが最終的に以下のいずれかを行うと仮定してもよいと述べています:終了する、ライブラリI/O関数を呼び出す、volatile glvalueにアクセスする、または同期操作またはアトミック操作を実行する。

while (true);ループはこれらのどれも行いません。C++26より前のフォワードプログレス規則の下では、このようなループに永遠に留まる実行は未定義動作でした。したがって、オプティマイザは実行が決してそこでスタックしないと仮定することができ、これによりループを削除し、パスを到達不能とマークする変換が可能になりました。

面白いのは、Cがこれを正しく扱っていたことです。C++11とC11はどちらもフォワードプログレス規則を導入しましたが、Cはさらに1つの規則を含んでいました:制御式が定数式であるループは、終了すると仮定してはならないのです。したがって、while (1);はC11以降で定義されています。

C++はこの追加規則を採用しませんでした。その結果、前述の不必要な分岐が生じましたが、繰り返します:while (1);はCでは定義されていましたが、C++では未定義動作でした。

しかし、そもそもなぜ誰かがwhile (true);を書くのでしょうか?

私が発見したところによると、これは組み込みシステムやカーネルコードでエラー発生時の停止パターンとして一般的です。致命的なエラーが発生し、終了するオペレーティングシステムがない場合、単に停止します。

if (hardware_init_failed()) {
    log_error("fatal: hardware init failed");
    while (true)
        ; // halt — there's nothing left to do
}

これはベアメタルで一般的なパターンであるだけでなく、C++では未定義動作でした。その結果は理論的なものではありません。オプティマイザがループを削除すると、実行は、リンカがその後に配置したコードにフォールスルーします — この記事の冒頭の「Hello world!」の例が示すように。組み込みシステムでは、それは致命的なエラーハンドラが実際にはデバイスを停止させないことを意味します。ハードウェアは破損した状態で実行を続け、後続する任意の命令を実行します。セキュリティが重要なコードでは、それは実際の脆弱性となります。

C++26の変更点

C++26はCの規則を単純にコピーするわけではありません。そのアプローチは検討され、却下されました。Cはより広範なループを保護します — 広義には、制御式が定数式であるループ — これは有用な最適化を妨げる可能性があります。代わりに、P2809R3は意図的に狭いカテゴリを定義します:自明な無限ループ。これは2つの条件によって定義されます。

  1. ループは自明な空の反復ステートメントである必要があります — これは、その本体が文字通り空(;または{})であることを意味します。たとえ意味のない式ステートメント("a string";など)であっても、非空のステートメントは失格となります。
  2. 制御式はtrueを評価する定数式である必要があります。条件のないforループの場合、trueは暗黙的です。

両方の条件が満たされると、ループ本体はstd::this_thread::yield()の呼び出しに置き換えられます。これにより、ループの実行に以前欠けていたフォワードプログレスセマンティクスが与えられます。

以下は、該当するものとしなかったものです。

コード自明な無限ループか?
while (true);はい
for (;;);はい
do {} while (true);はい
constexpr bool go = true; while (go);はい — goは定数式です
while (true) { "a string"; }いいえ — 本体にステートメントが含まれています
while (true) if (done) break;いいえ — 本体が空ではありません
while (true) if constexpr (false) break;いいえ — 自明な空の反復ステートメントの構文に一致しません
bool done = false; while (!done);いいえ — 定数式ではありません

この変更はフォワードプログレス保証自体も更新します:スレッドは、最終的に行うことの1つとして、「自明な無限ループの実行を継続する」ことが now 許容されるようになりました。したがって、オプティマイザはもはや自明な無限ループを未定義動作として扱い、実行がそれを通過すると仮定することはできなくなりました。

フリースタンディング実装に関する注意点

フリースタンディング実装では、std::this_thread::yield()への置き換えが発生するかどうかは実装定義です。これはベアメタルシステムにとって重要です。意図的な停止ループを協調的なyieldに変換すると、プログラマが意図しなかった動作が発生する可能性があります。

結論

while (true);が未定義動作であったことは、誰もが聞いて驚くC++の事実の1つでした。それはCからの不必要な逸脱であり、実際の組み込みコードを壊し、コンパイラはそれを実際に悪用していました。C++26はこれを修正します — 自明な無限ループは定義された動作になり、コンパイラはもはやそれを最適化して削除することはできません。

さらに深くつながる

この記事が気に入ったら、ぜひ