C++ 再入門 その40 - 例外の伝搬と最適化
前回、例外を受け取るクラスを用意して、それを使って例外処理を書く方法を説明しましたが、C++の例外処理はなかなかクセがあって使いこなすのは難しいです。
C++ 再入門 その39 - 例外のためのクラス
try 節の中で例外が発生するまたは throw 文が実行されると、それは catch 節 に実行が移るわけですが、その関数に try 節が無くてもその関数の呼び出しが try 節の中にあれば、呼び出し側の catch 節に飛んでいくわけです(これは main() に至るまで try があるか遡っていく)。
carch された例外は、そこで何らかの処理を行い、対応する try を抜けたところから実行が再開されますが、catch 節に throw 文があれば、さらに外側や呼び出し側の catch が検索されて実行されるコードがそこに移るわけです。これは実行する際の呼び出し関係で決まるので、コードを静的に見てもすぐにわかるものではなく、例外発生時(throw文の実行を含む)にいったいどこに制御が移るのかの判断はコードを書いている人にはなかなか難しいです(CPUはスタックを遡るだけなのでちっとも困らない)。
第 5 章 例外処理
実は例外発生時に同じスコープ内に catch が無ければスコープや関数から抜け出すことになるので、そこでデストラクタの実行やスタックフレームの巻き戻しも行われています。ですから当然その為のコードもどこかに埋め込まれているわけで(そして引き数を戻り値のごとく catch 節に引き渡す)、例外処理を実行するのはCPUにとっても簡単なことでは無いのです、
例外処理のコストとして思い出したのが VB の例外処理の実装です。もともとBASICという言語はインタプリタとして実装されていたので、言語仕様もそれを前提としているところがあって ON ERROR GOTO 文の最後では RESUME または RESUME NEXT 文で、エラーの発生した行を再実行したり、次の行から実行を再開できる仕組みがあります。インタプリタであればBASICには関数のスコープも無いので簡単な処理なのですが、これをコンパイルするとなると、エラーの発生に備えて行番号が書かれている場所全てに戻るべき場所をエラー処理のためのスタックに退避しておく必要があり、実際にそのコードが行番号のあるところすべてに展開されていました。これは特にすべての行に行番号が必要だった時代のコードにおいてはなかなか馬鹿にならない処理で、どうしてこんなにコードサイズが大きくなるのだろうと調べて発見しました(可能な限り RESUME 行番号 の構文を使うように直しました)。
幸い C++ では例外処理の後に、元の場所に戻ることは無いので(VBでも新しい構文である構造化例外を使えば同様)、そこまでの影響は無いのですが例外が起きた時に catch 節を探して処理を移すためのコードが必要であることには違いありません。
C++の例外ハンドラを自作してみる。
例外処理の状態を細かく把握したくなる状況は、それなりに発生して、例外処理に関する処理が定義されている exception をインクルードすると、実に細かな定義が用意されていることがわかります(そしてC++の進化に伴い多くの変更もある)。
exception
C++コンパイラは基本的にはCPUが直接実行するバイナリを生成するので例外処理のようなCPU自身の仕組みにないコードを生成し実行する(そして例外と関係ないライブラリも直接に呼び出す)には、それなりのコストが必要になります。
プログラミング言語にある例外機構はthrowするとコストがかかるっぽい
ソースコード上に現れないコードがいろいろと必要になるということは、最適化処理を行う際にもコンパイラが調べる範囲が膨らむということで、例外の無い既存のC言語のコードであるとかライブラリを呼ぶときくらいはコストを節約したいということで、nothrow という修飾子があったりもするのですが、これはこれで副作用も大きいです。
C++のエラー処理との付き合い方
このような反省もあって、Go言語では例外処理の機能を用意していませんし、Rustにも例外がありません。
エラー処理 - Rust
このようにC++で例外を正確に漏れなく処理することは実はかなり大変なので、プロジェクトで例外の使用を禁じている会社もそれなりにあるようです。そしてこれは組み込みの場合には特に深刻で処理系によってはそもそも例外をサポートしていないこともあるようですし、例外処理によってコードサイズが膨らんだり、例外処理の最中にリソースが足りなくなる事故を避けるために仮に使っている部分があるとしても(ライブラリは例外を前提に書かれていることも多い)、可能な限り排除する努力をしていることも多いようです。
4.4 組込み開発では例外処理を使用すべきか?
確かに例外を考慮してブランチカバレッジをテストしようとしても現実的ではない部分もありますし、例外のうちリソース不足が原因のものは細心の注意を払って処理コードを用意しておかないと例外処理自体がちゃんと実行できません(全部 static で処理してね、あ~スレッドが…)。
とはいえ例外が無い時代のように、どこかでエラーが起きてもそれを把握できずプロセスが落ちてしまうようでは本末転倒なのですが、プログラムの内側でセッセとエラーを捕まえる努力をするよりは、プロセス自体を監視して、落ちてしまってから適切な処理を他のプログラムで行うのもクラウドな時代には悪くはありません。異常に対する対処も時代とともに変わっていくものでしょう。ということで例外に関してはまだまだ奥深いものが残っているのですが、これでいったんは終わりとしたいと思います。
ヘッダ画像は、以下のものを使わせていただきました。https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による
#プログラミング #プログラミング言語 #プログラミング講座 #CPP #例外処理 #exception #例外の伝搬 #最適化 #エラー処理
いいなと思ったら応援しよう!
頂いたチップは記事を書くための資料を揃えるために使わせていただきます!