C++26の進化:ついに「空の無限ループ」が未定義動作から解放される!
C++26: Trivial infinite loops are no longer undefined behaviour
C++26: Trivial infinite loops are no longer undefined behaviour
C++標準化委員会によって、長年の議論となっていた重大な変更が承認されました。これまでC++において、副作用を伴わない空の無限ループ(Trivial infinite loops)は「未定義動作(Undefined Behaviour)」として扱われてきましたが、C++26からはこれが正式に言語仕様として許可されることになります。これにより、コンパイラがループを不正なコードとみなして削除してしまうといった挙動が回避され、低レイヤーのプログラミングやOS開発などにおいて、より直感的かつ安全な実装が可能になります。
あの例で unreachable() 関数が実行されるなんて想像もしなかったよ。まあ実務で遭遇するような代物ではないだろうけど、#ifdef が重なったりすると変な挙動をすることはあるよね。
関数が別の関数に「フォールスルー(すり抜け)」するなんて考えたこともなかったな。そもそもなんでこんな挙動が存在するわけ?
両方の条件が満たされると、ループ本体が std::this_thread::yield() の呼び出しに置き換わる。
悲鳴を上げたい気分だ。
ライブラリ呼び出しが一切ない無限ループに「システムコール」が挿入されるなんて。いつか大惨事を引き起こす最悪のサプライズだよ。
「フォワードプログレス(前方進行)保証」という概念自体が破綻してるね。無限ループは無限ループとしてコンパイルされるべきであって、それ以上でも以下でもないはずだ。
この記事、肝心の「なぜ無限ループを未定義動作(UB)にしたい人がいるのか」という理由が書かれてないのが残念。この説明を見つけたよ: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528.htm
ループは自明な空の反復文でなければならない――つまり、本体が文字通り空であること
これだとループ本体が "continue" だとダメってことみたいだな。実際に -std=c++26 で ";" を試したら期待通り無限ループになったけど、"continue" だと未定義動作が復活したよ:
"while(true);" -> https://godbolt.org/z/T65o51crx
"while(true) continue;" -> https://godbolt.org/z/Pj9raEcnP
セミコロン単体よりも "continue" を好むスタイルガイドを知ってるから、これは困るな。これからはみんな "while(true) {}" を使うことになるんだろうね。
両方の条件が満たされると、ループ本体が std::this_thread::yield() に置き換わる。これにより、ループ実行に欠けていた前方進行のセマンティクスが与えられる。
これぞ Linus や多くの人が C++ を嫌う理由である「隠れたコード」の弊害そのものだね。コンストラクタやデストラクタならある程度は仕方ないし、ランダムでもない。Rust は少なくともドロップ時の非局所的なコードの影響範囲を抑えるのが上手い。
C11 のルールを採用したくないなら、C++ 委員会は trivial なループ(C11 の定義かどうかにかかわらず)に対してコンパイラに診断やエラーを出させ、プログラマに ::yield 等を明示させるルールを検討すべきだったよ。隠れたコードなんてないほうがいいし、コンパイラが驚くような動きをする余地も減る。
C 委員会は標準規格内の未定義動作ケースを厳密に列挙して、診断やエラーを必須にしたり、処理系定義の動作にしたりして一つずつ対処してきた。でも、こんな風に勝手にコードを挿入するなんて考えられないよ。
一般的にはそうでもないと思われてるみたいだけど、マイコンプログラミングにおいて無限ループ while(1) が有効なケースは結構あるよ。STM32 の自動生成された HAL コードはエラーハンドラでそれを使ってるし、C++ もサポートしてるから、これが未定義動作扱いになるのは驚きだ。
自分はエラーハンドリングのためだけに使うけど、もちろん省電力重視のアプリで待機やスタールにこれを使うのは悪手で、その場合は割り込みからの復帰を使うべきだね。
ちなみに、自分はエラーハンドラにソフトウェアブレークポイントを入れるのが好きだよ。これだと物理的に数が限られているハードウェアブレークポイントを消費せずにデバッグできるからね。
__BKPT();
while (1)
;
[C++ で C のルールが却下されたのは] 有用な最適化を妨げる可能性があるため
この「有用な最適化」とやらが、自分にとってこの新しい厄介な挙動を受け入れてまで得たいほどのものなのか興味があるね。
それとも、コンパイラ開発者が競合チームとの長年にわたる意地の張り合いの中で勝手に「有用だ」と思い込んでるだけじゃないの?
勘違いしないでほしいんだけど、自分自身も競合チームと長年意地の張り合いをしたことがあるよ。あれは楽しいんだ。でも、それをユーザーのためだなんて自分たちを騙すのはやめようよ。
どこが有用なのか全くわからない。あんなループを書くのはほぼ100%エラーだし。唯一存在意義があるとしたら超低レベルコードで何もしない場合だけど、そんなのアセンブリで書いた外部関数を使えば済む話だし、C++ の標準規格をいじる必要なんてないよ。メリットはほぼないのに規格を複雑にして難易度を上げてるだけじゃないか。
処理系は、どのスレッドも最終的には終了するか、ライブラリの I/O 関数を呼び出すか、volatile な glvalue にアクセスするか、同期あるいはアトミック操作を行うと仮定してもよい
なんでこのルールが必要なの? for ループで停止問題の解法を試みるようなコードを書けば、どのみちループは終わらないから、このルールを回避できちゃうんじゃないの?