C++26、無限ループを未定義動作から救う
C++26: Trivial infinite loops are no longer undefined behaviour
C++ではwhile(true);のような無限ループが未定義動作とされ、Clangはループを削除してリンカが配置した関数を実行することがあった。C++26のP2809R3は、本体が空で制御式が定数trueの「自明な無限ループ」を明確に定義し、std::this_thread::yield()呼び出しに置き換える。組み込みやカーネルで使われる停止パターンの安全性が向上する。
C++26はこれをP2809R3で修正する。自明な無限ループはもはや明確に定義される。
HNでの議論
75- JoshTriplett
> 両方の条件が満たされると、ループ本体は std::this_thread::yield() の呼び出しに置き換えられる。
ここで叫び声を上げてください。
ライブラリ呼び出しが一切ない無限ループに、システムコールが挿入される。それは待ち受ける恐ろしい驚きだ。
「forward progress guarantee」という概念全体が壊れている。無限ループは無限ループにコンパイルされるべきだ。それ以上でもそれ以下でもない。
- omoikane
> ループは自明に空の反復文でなければならない -- つまりその本体は文字通り空である
これはループ本体が「continue」であってはならないと言っているように思える。実際、-std=c++26 で ";" を試したところ、約束通り無限ループになったが、"continue" では未定義動作が復活する:
- "while(true);" -> https://godbolt.org/z/T65o51crx
- "while(true) continue;" -> https://godbolt.org/z/Pj9raEcnP
これは残念だ。なぜなら、単一のセミコロンよりも "continue" を好むスタイルガイドを一つ知っているからだ。おそらく、そうしたコードは今後すべて "while(true) {}" になるだろう。
https://google.github.io/styleguide/cppguide.html#Formatting...
- Aurornis
あの例で unreachable() 関数が実行されるとは全く予想していなかった。実際には遭遇しないだろうが、#ifdef の層で奇妙なことが起こるのは見たことがある。
- wahern
> 両方の条件が満たされると、ループ本体は std::this_thread::yield() の呼び出しに置き換えられる。これにより、ループの実行に以前は欠けていた forward-progress セマンティクスが与えられる。
これは、Linus や他の多くの人々が C++ について嫌う隠れたコードの欠点の典型例だ。コンストラクタとデストラクタについては、ある程度避けられず、それほどランダムではないが、Rust は少なくともドロップの場合において、非ローカルコードの影響範囲を制限するのが上手い。
もし C11 のルールを採用したくなかったのなら、C++ 委員会は、自明なループ(C11 で定義されているか否かに関わらず)に対してコンパイラが診断またはエラーを出すことを要求し、プログラマに明示的に ::yield などを挿入させるルールを検討すべきだった。隠れたコードはなく、コンパイラが予想外のことをする機会も減る。
C 委員会は、標準で UB ケースを厳密に列挙し、多くの場合、診断やエラーを要求するか、実装定義の動作に変えることで、各ケースに順番に対処してきた。しかし、そのようなコードを挿入することは考えられない。
- peterus
マイクロコントローラプログラミングにおいて、無限 while(1) ループには有効なユースケースがある(一般通念に反するようだが)。stm32 用の自動生成 HAL コードはエラーハンドラにこれを使い、C++ もサポートしているので、これが UB だったのは驚きだ。
私はエラーハンドリングにしか使わないし、もちろん電力に敏感なアプリケーションで待機/ストールにこれを使うのは悪い考えだ。その場合は割り込みからのウェイクアップを使うべきだ。
余談だが、私はエラーハンドラにソフトウェアブレークポイントを入れるのが好きだ。ハードウェアブレークポイント(マイクロコントローラによって物理的に制限される)を無駄にせず、デバッグが容易になる:
__BKPT();
while (1)
;