C++での例外。function-try-block

(以下、あまりに乱文なので適宜書き直し中です。眠いときに長文はダメです)

C++の例外は使いにくい。なにせ、

  • 遅い
  • アホほど飛ぶ。嫌になって catch (...) しそうになる
  • デストラクタがあるので、結局重要な局面ではいらなかったりする。finally 無いし

なのだから。というわけで、実務では実はあまり使う機会がない。実務では速度優先だ。
しかしながら、本当にそこまでダメなものなのかな? とも思う。finally が無いことが目の敵にされるけど、それなりに意味が有るのではないかとも思う。
以下、「こんなプログラムを書いてみたい」という妄想のお話なので、真に受けないでいただきたい。
そもそも、C++の例外はどういうときに使えばいいのか難しい。もっと例外が染み着いた言語とは根本的に違う感じがする。
http://oshiete1.goo.ne.jp/qa2831606.html の 3 番の回答というのが、的を射ていると思う。

送出された例外を処理することができない箇所に監視ブロックを入れてみても、どうすることもできずに、(臭いものに蓋をするように)単に例外を捨ててみたり、abort等で異常終了させるしかなくなります。例外が発生したときに、リソースの解放や状態の巻き戻しを行うためだけであれば、監視ブロックではなくデストラクタを使うべきです(Javaなら仕方ないでしょうが)。

とのこと。なぜfinallyが無いのか。必要ないからだ…なのか、思い付かなかったのかは知らないが、例外の多くがRAIIの適切な使用で省略できるのは本当かもしれない。



実務で例外を使わないといったが、ではどうしているかというと、とても泥くさい。assert に似たようなマクロ等で処理を強制的に止めるか、ダミーの値を返している。
ダミー値を返すのは良い方法のはずだ。エラー時の挙動が読み易く、理解し易いプログラムとは、エラー処理自体が(ほとんど)存在しないものだ。
エラーコードを返すなど愚の骨頂…と考えている。



ここからは、例外は使えない状況を考えていただきたい。
以下のコードを考えてみよう

    if exists_directory("/hoge/") then
        if exists_file("/hoge/fuga") then
            if can_read_file("/hoge/fuga") then
                #...

もう、一体いつになればファイルが開けるのか分からない…。本当のプログラムではこうはなっていないはずだ (いや、Windows APIはこうなっている気がしてきた…)。

    file_handle = open_file("/hoge/fuga")
    if is_opened(file_handle) then
        #...

というのが一般的だろう。これは実際、「エラーコードを返す」という処理を行っていない。そうではなく、「無効なfile_handle」を返すことにより、エラーであったとしても、問題なく処理が進むようになっている。(例外のcatchによる処理の分離に相当する)



実はこの設計は、例外機構より良いのでは無いかとすら思っている。少なくとも、C++のように例外が (処理的、使い易さ、標準に準拠したコンパイラの数から) 高価な場合は。ひとつの理由は、起こりうるエラー状態をオブジェクト側 (file_handle側) で制御できる点だ。つまり、

    try
        data = read(file_handle)
    catch cannot_open
        # 開けなかったかもしれない
    catch cannot_read
        # 読めなかったかもしれない
    catch not_enough_memory
        # メモリが足りなかったかもしれない
    #...

ではなく、

    data = read(file_handle)
    if succeeded(file_handle) then
        #...

の方が、例外もfile_handle内にカプセル化できて良いのではないかと思うのだ。実際、プログラムの処理としては、ファイルが読めなかったときは

  • 必要なファイルなら、エラー表示を出す
  • 必要でなかったら、デフォルト値を設定する

くらいしかする処理は無いのだから、succeeded ひとつで処理を終わらせた方が良い。標準ライブラリのファイルストリームは伊達や酔狂で is_open() メソッドを持っているわけではない。
細かなエラー情報はエラー表示の段で得られれば十分だ。
ということは、assertなどでエラーを引っかけるのは、開発環境の問題がない限りは、やってはいけないと言える。デバッガが使える環境でないと、エラーコードを知る術すら、クラスやメソッドの外部にはない。再利用性のかけらもないプログラムが生まれてしまう。
実務でも、再利用時に邪魔なassertを取るためにコードの大幅書きなおしをすることがよくある。



「より重要なのは、関数内で、なんらかのエラー状態に遭遇したために、残りの処理が続けられなくなった際、すぐにreturnできること (例外のthrowに相当する) と」考えることもできる。
エラー毎に変な一時オブジェクトを作るのはどうも気にくわないし、難しい場合がある。堅牢制確保の一貫で、一時オブジェクトを作れないようにしたのに、エラー処理だけのためにデフォルトコンストラクタを追加し、しかもデフォルトコンストラクタされたインスタンスを「エラー時に何か返さないといけなかったから…」という副次的な理由でのみしようすることほど、やる気の削がれることはない。
となると、関数は可能な限り、void 型にすべきで、設計上、例外を許さない場合のみ返値を返すという設計がありうる。例外を使わないのならエラーはダミーオブジェクトで表すことになるだろう。



さて、なぜ私は例外を使おうとしていないのか。実務で使えないということ、思考実験をしたかったということもあるが、そもそも例外が好きではないという理由がある。
上で、エラー検知のために if を重ねる例を示したが、例外ってそんなものではないかと思うのだ。以下のプログラムを見ていただきたい。

    file1 = open_file("hoge")
    file2 = open_file("fuga")
    if succeeded(file1) or succeeded(file2) then
        read_file_and_add_to_database(file1)
        read_file_and_add_to_database(file2)
    else
        print error_of(file1), error_of(file2)

つまりは、2つのファイルを読み込み、読み込めた分だけを使用しているわけだ。(このシステムではどちらかのファイルが存在しない場合があるらしい)
通常の例外は、「例外的な状況を正常な状況として扱う」のが不得手だ。入門書には「例外的な状況」と「正常な状況」を分けて、前者にのみ例外を…というが、そんなのは不可能だ。場合により同じ状況が例外か正常かは変わる。
上の例だと、open_file は何種類もの例外を投げる可能性がある。いちいち全てcatchするのだろうか? 堅牢性というが、反対に本当に取扱いの難しい処理の場合、例外ではおおざっぱ過ぎる。throwは勝手にコールスタックを遡るので、「処理を続けますか?」ダイアログを出すのすら難しい。せいぜい、better assert 程度にしか使えない。なんだかどっちつかずなのだ。



上の例であるように、

  • オブジェクトは、何が有ろうとも、自身がクラッシュしないように動作する
  • エラー状態を検知し、適切な処理をするのは、オブジェクトを使用する側

というのが良いのではないかと考えている。
世の中がそうなっていないのには、それなりの理由があるのが歯がゆいが、がんばってみたい。



と書いてきたが、本当は

  • 例外処理と正常処理を適切に分割でき、同時に適切に結合できる
  • 全ての例外が catch されることが保証される文法を持つ

関数型言語、というのが本当の姿だとは思う。


本題

と、長々と書いてきたのは、C++のこの文法を知らなかったのだ…ということが書きたかっただけだったり。

    // コンストラクタ
    Object::Object()
        try
        {
            : member()
        }
        catch (exception_from_member e)
        {
            // ...
        }

function-try-block (関数監視ブロック) と呼ぶらしい。member のコンストラクタ内での例外をcatchする方法。catchの最後で勝手に例外が再throwされるのに注意。