見出し画像

エラー処理あれこれ

プログラムを書いて、コンパイラを使う言語であればコンパイルして、エラーが出なくなったので実行すると、どういう訳かエラーメッセージが出てプログラムが終わってしまうことがあります。

これを俗に「ランタイムエラー(実行時エラー)」と呼ぶのですが、これにはいくつかの原因があります。例えば、開こうとしているファイルが何らかの理由で開けない、割り算で0で割ってしまった、メモリが足りなくなった。などです。

もっとややこしい理由の場合もありますが、この時に出力されるメッセージはプログラムを書いている本人であればまだしも、ソースコードを見ることがない人には何を言いたいのかわからないのが普通です。開発中であればともかく人に使わせるプログラムを書くのであれば、このようなメッセージが出てプログラムが終わることのないように、何らかの処理を行って、プログラムが継続できない場合であっても、どうすれば良いのかわかるように知らせる必要があります。

このように実行時にエラーが出た時に、それに対処することをエラー処理とか例外処理などと呼びます。プログラミング言語自身にエラー処理が書けるようになったのは、いつ頃なのかを調べてみたのですが、FORTRANであればファイル操作のERR指定子、COBOLであれば入出力のUSE文があるくらいで、大域的なエラー処理は、もしかしたらAPPLESOFT BASICのONERR GOTO文が最初かもしれません。

やはりコンパイル言語は実行時にはマシン語になってしまうので、エラー処理を行うのはなかなか大変です。インタプリタであるBASICであるからこそ、エラー処理を行えるようになったのかもしれません。もっともコンパイル言語の時代であっても、プログラムを制御するJCLにはエラー処理があったのではないかと思います(ここは詳しくない)。

APPLESOFT BASICのエラー処理に関する構文は、実にシンプルです。

ONERR GOTO [行番号]

という文を実行すると、その時点からエラーが発生したら指定した行番号に飛ぶようになります。この文を実行するまでと、ONERR GOTO 0 (行番号0は許されていない)を実行してからは、エラーが出るとエラーメッセージを表示してプログラムは終了します。
(※後のBASICでは ON ERROR GOTO と少し予約語が変わっています)

指定された行番号からエラー処理を書きます。処理が済んだらRESUME文を実行します。これでエラーが発生したところから再開します。エラーが発生したところから再開するので、エラー処理の中でエラーとなった原因を解決しないと、再びエラーになってしまいます。またエラー処理の中でエラーを出してしまうと、そこでエラー処理の先頭から実行されてしまいプログラムが無限ループに入ってしまいハングアップしてしまいます。ということで、まだちょっと使いにくいところもありました。なおエラーの原因となったエラーコードは特定のメモリに入っているので、PEEKで読み出して判断します。

Applesoft BASIC Quick Reference

Applesoft BASIC Programming Reference Manual

https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Software/Languages/Applesoft%20BASIC/Manuals/Applesoft%20II%20BASIC%20Programming%20Reference%20Manual.pdf

ONERR GOTO のお陰で、ディスクの蓋が開いていてファイルが読めないときであるとか、ファイルを書いている最中にCTRL-Cで中断されてファイルが中途半端なことになってしまうなんて言うときの対応ができるようになりました。

このあたりは後のBASICではいろいろ改善され、エラー処理の最後で RESUME NEXT文を実行することで、エラーを起こした次の文から実行が再開するように出来たり、エラー処理の中で起きたエラーで自分自身に飛ばなくなったりしています。ただ一部のBASICでは(ENDではなく)STOPで止まった場合、ダイレクトモードに戻ってもON ERROR GOTOが生きていて実に面倒なことになるなんていうのもありました。

最近のVisualBASICではC++やC#のような例外処理が実装されているので、行番号を使わなければならないON ERROR GOTOは推奨されなくなっているようです。ちなみにBASICであってもコンパイルするようなBASICの場合、どこでエラーが発生するかわからなので、エラー処理中で使うエラーが発生した行番号を得るのと、RESUMEで戻るために、行ごと文ごとに、戻るべき場所を特定のメモリに格納するコードが必要になるようで(デバッガで確認しました)、バイナリサイズがとても大きくなったのを覚えています。この構文はインタプリタであるからこその作りなので、移植をさぼる場合は仕方がないとしても、コンパイルする場合には最近の形式にするのが良さそうです。

Cの場合はどうするのかというと、Cには言語としてのエラー処理がありません。コンパイル言語ですからマシン語で扱えるエラー処理である割り込みくらいしか出来ることはありません。割り込みに関してはOSのお仕事なので、Cから見た場合は signal というライブラリで、これを制御します。このあたりはOSと関わる深い部分なのでライブラリをよく読んでもらうとして、Cの場合はライブラリ関数を呼び出して処理が終わった場合、関数の戻り値としてエラーが発生したことを示す値が返ってくることがあるので、これを逐一チェックするのが基本になります。普段のプログラミングでは、これをサボっていることがよくあるのですが、これはたまたまエラーが起きていないから問題が出ないだけで、人に使ってもらうプログラムを書くのであれば「必ず」エラーをチェックしなければいけません。

C++やC#などのモダンな言語では、try/catchという構文があって try のスコープの中で起きたエラーは(運が良ければ)catchで拾うように出来ます。もっともCのライブラリを呼んだ場合などはライブラリで例外を出してくれないので、やはり戻り値をチェックして自力で throw する必要があったりします。この構文のお陰でエラーに対処するのは楽になりましたが、tryを使っていないコードはまだ多いですね。

例外処理

エラーを拾うようにできるようになっても、どう対応するかは難しくて、結局、エラーが発生した処理を、やり直すか、スキップするか、何もしないかになります。これを呼び出した処理に伝えるのですが、適切な情報をセットアップしてからでないと、呼び出し元では何が起こったのかわからないので、結局、何もできないことになったりします。古のBASICのように変数にスコープがなければ楽なのですが、C++ではスコープを抜けると変数の値が無くなってしまいますし、(スレッドの中とかの)例外発生時にはスタックフレームが壊れることがあるので、慎重な書き方が必要です。

エラー処理を丁寧に書くことは、プログラムの安定した動作をさせるためにはとても大事なことで、ここがプロのプログラマーの腕の見せどころでもあります。ここが手抜きだと、だんだんメモリなどのリソースが無くなっていったり、時折、異常な動作をしたりすることになります。ただ何事にもバランスがあって、ここを頑張り過ぎでもハードウェアの異常などはどうにもならないこともあります。過去に頑張りすぎてプログラムは10年以上動き続けたのに、頑張ったエラー処理をしたログが最後まで一度も出なかったこともありました。

具体的なエラー処理の書き方は言語ごとに具体例を挙げながらでないと、ちょっとわからないとは思います。これを書き始めるととても長くなるので端折りますが、定番の「型」として身につけると、そんなに面倒ではなくなります。またエラー処理にはレイヤーがあって、関数レベルやプログラム(プロセス)レベル、OSレベル(最近ではマシンやクラスタレベルまで)それぞれで対処する必要があるので、広範な知識と経験が必要で奥が深く挑戦のしがいがあるテーマです。

ヘッダ画像は、以下のものを使わせていただきました。

https://www.irasutoya.com/2015/07/blog-post_993.html

この記事が気に入ったらサポートをしてみませんか?