Boost.Exceptionの紹介。

Boost.Exceptionの紹介。

どうも。宮崎のヌーさんから紹介にあずかりました北海道の魔法少女です。
この記事はBoost Advent Calendar 2011の参加記事です。


今回は、Boost.Exceptionについて紹介します。


はじめに。

Boost.Exceptionとは。
Boostのライブラリの一つで、C++における例外ハンドリングを扱います。
Introductionによると、Boost.Exceptionの目的は、例外クラス階層のデザインを容易にし、さらに例外ハンドリングとエラーレポートのコーディングを支援する、らしいです。

従来の例外処理

従来の例外処理では、例外をスローする側で

  • エラーに対して適切な型で例外オブジェクト生成して、
  • それに、発生したエラーについてのデータを突っ込んで

スローします。

そして、上位のコンテキストでそれをキャッチしたら

  • 例外の型を元に何が失敗したのかを選択して、
  • その例外オブジェクトの持つデータを検査して、問題に対処

します。

で、従来のアプローチだと得てして、例外がスローされる位置では、エラーがハンドリングされるためのcatchの側で必要なデータが不足していたりします。


たとえばファイルの読み込みエラーの場合、例外を受ける側ではどのファイルの読み込み時にエラーが起きたのか、ファイル名が欲しかったりしますが、

catch( file_read_error & e )
    {
    std::cerr << e.file_name();
    }


次のようにファイルポインタでファイルを読み込むような実装の場合、エラーが起きた場所だとファイルポインタしか知らないので、例外情報にファイル名を含めることは不可能です。

void
read_file( FILE * f )
    {
    ....
    size_t nr=fread(buf,1,count,f);
    if( ferror(f) )
        throw file_read_error(???);
    ....
    }


ここでread_fileを次のように、ファイル名も渡すようにすればいいかというと、

void
read_file( FILE * f, char const * name )
    {
    ....
    size_t nr=fread(buf,1,count,f);
    if( ferror(f) )
        throw file_read_error(name);
    ....
    }

これは現実的な解決にはなりません。
一般的に、ライブラリから発生する例外のうち、必要とされるデータというのは、そのライブラリにリンクしているプログラムよって異なります。そしてthrowからcatchまでの間のたくさんのコンテキストにはそれぞれ例外ハンドラに転送されるべきさまざまな情報があります。


というわけで、下位の関数では情報が足りないので、ここだけで例外の送出を何とかするのは無理があります。
catchまでの上位の関数の協力が不可欠です。

例外のラッピング

下位の関数から例外をcatchしたときに、その例外を内部にもつ新たな例外として(そして今の例だと、ファイル名なども付加して)、上位に新たな例外を送出する方法があります。(特にJavaをバックグラウンドにもつC++erには一般的な方法だってばっちゃが言ってた!嘘、ドキュメントに書いてあった。)

しかしこれには,

  • 例外のコピーによってスライシングが起きるかもしれない。
  • ジェネリックなコンテキストでは例外のラップは実際には不可能。

という問題があります。
二点目の方は、例外中立の原則に違反する特別なケースです。(とかドキュメントに書かれてましたがあんまり言ってる意味が分かんなかったです。つまりジェネリックなコンテキストだと渡って来る型がわからんから全てに対応して例外のラッピングとか無理やし。ってことかしら)

Boost.Exceptionが提供する解決策

という訳でboost::exceptionによる解決策です。
ドキュメントからコードを拝借して(若干変更を加えてます)、解説していきます

struct exception_base: virtual std::exception, virtual boost::exception { };
struct io_error: virtual exception_base { };

このサンプルで登場する例外クラス階層のベースとなるクラスとしてexception_baseを、そして、そこからさらにio_errorというIOに関するエラーを定義しています。


exception_baseはstd::exceptionとboost::exceptionを仮想継承しています。
ほかの派生クラスも、ベースとなるクラスを仮想継承します。
これらの仮想継承ついては、
using virtual inheritance in exception types - 1.70.0に詳しい解説があります。
つまり、例外クラスを多重継承したときに、同じ基底クラスが複数存在してしまうと基底クラスの型で例外をキャッチしようとしたときに曖昧性が生じて意図しない結果となることを回避します。例外オブジェクトは例外的な状況で使用されることを想定されているので、これら例外クラスにおける仮想継承によって導入されるコストはほとんど無視できるものです。

struct file_read_error: virtual io_error { };

struct tag_errno_code {};
typedef boost::error_info<struct tag_errno_code,int> errno_code;

void
read_file( FILE * f )
    {
    ....
    size_t nr=fread(buf,1,count,f);
    if( ferror(f) )
        throw file_read_error() << errno_code(errno); //(1)
    ....
    }

read_fileのエラー処理はこのようになりました。


まず、fileの読み込みに関する例外の型として、file_read_errorを定義しています。
そして次に、boost::error_infoを特殊化して、例外情報を保持するクラスとしてerrno_codeを定義しています。

boost::error_infoに指定できる型のRequirementsなどはこちらに詳しくあります。
error_info - 1.70.0
テンプレートの第一引数にどのエラーかを表すタグ、第二引数に保持するエラーを指定します。

tagが指定できるので、区別したいエラー情報だけど保持したい型がかぶってしまう場合、例えば何種類かのライブラリを使用していて、それぞれがエラーコードに対してint型を使っているなどの状況で、それぞれのライブラリごとにタグを用意すれば、エラーについての情報が混ざることなく扱えます。tagはerror_infoの特殊化ごとに別の型であることが区別出来ればいいので、上記のtag_errno_codeのように実装が空の型とかでもいいです。


エラーが起きた場合、(1)で例外を送出しています。
まず、boost::exceptionから派生したクラスfile_read_errorを構築して、operator<<で上記で定義したerrno_codeを流し込んでいます。
errno_codeは保持する型をコンストラクタで受け取るので、errno_codeの構築の際にerrnoを渡しています。

そして、file_read_errorがoperator<<でerrno_codeを受け取ったあと、operator<<は左辺の引数であるfile_read_error(のconst参照)を返すので、そのままthrowしています。


read_fileを呼び出す上位の関数では次のようにして例外情報を追加します

struct file_open_error: virtual io_error { };
typedef boost::error_info<struct tag_file_name,std::string> file_name;

void open_read_file()
{
    try
        {
        if( FILE * fp=fopen("foo.txt","rt") ) //(2)
            {
            shared_ptr<FILE> f(fp,fclose);
                //....
            read_file(fp); //throws types deriving from boost::exception
            //do_something();
                //....
            }
        else
            throw file_open_error() << errno_code(errno); //(3)
        }
    catch( boost::exception & e ) //(4)
        {
        e << file_name("foo.txt");
        throw;
        }
}

最初に、ファイルオープンに関するエラーfile_open_errorと、
ファイル名を例外に付加したいので、file_nameというboost::error_infoクラスを定義します。
open_read_file関数という関数がtry句の中の(2)で、"foo.txt"というファイル名でファイルをオープンし、read_file関数を呼び出しています。

ファイルのオープンが失敗した場合は、失敗した状況についてのerrnoをfile_open_errorに含めて例外を送出しています。(3)

ファイルオープンやread_file関数で例外が起きた場合は、このopen_read_file関数のcatch句に処理が渡ってきます。(4)
file_open_errorもread_fileで送出される例外オブジェクトの型file_read_errorも、どちらもboost::exceptionを継承しているので、catchした例外をboost::exception &で受けとることが出来ます。
そして、受け取った例外オブジェクトeに対して、新たにfile_nameという例外情報を追加しています。
そして、現在の例外を上位に再スローします。


open_read_file関数内で例外にファイル名の情報を付加することが出来ました。
この時、open_read_fileは、送出されてきた例外については、boost::exceptionであるということ以外を知っている必要がないというのは重要です。

open_read_fileでは、read_fileから送出された例外をcatchしてエラーハンドリングをするわけではないので、例外を勝手に飲み込んだりしないで、より上位の関数に正しく伝える必要があります。(これを例外中立といいます)


open_read_fileではread_file関数が送出する例外にどんな例外情報が付加されているかを気にする必要はありません。
たとえば上記のdo_something()関数が、こっそりlyrical_magical_error_infoをfile_read_errorに付加して例外を送出したとしても、open_read_file関数では気にすること無くfile_nameを付加してあげればいいだけです。名前を呼んでって白い悪魔に言われたらboost::exception &って答えておけばいいんです。


より上位の関数で、この例外がどのようにハンドリングされるかを見ていきましょう。

void open_read_and_do_something()
{
    try {
        open_read_file();
        //do_something2();
    }
    catch( io_error & e )
        {
        std::cerr << "I/O Error!\n";

        if( std::string const * fn=get_error_info<file_name>(e) )
            std::cerr << "File name: " << *fn << "\n";

        if( int const * c=get_error_info<errno_code>(e) )
            std::cerr << "OS says: " << strerror(*c) << "\n";
        }
}

open_read_and_do_something関数では、open_read_file関数を呼び出して、エラーが発生した場合は、catch句でエラー情報をstd::cerrに表示しています。

catchする例外の型は、file_read_errorやfile_open_errorの抽象的な型、io_errorで受け取っています。
boost::exceptionから派生している例外クラスの階層において、例外の型は派生クラスを定義してそれぞれのデータメンバでふさわしいエラー情報を保持するしくみより、ただの例外情報のタグとして使用されます。
exception types as simple semantic tags - 1.70.0


boost::exceptionに含まれる例外情報はget_error_infoテンプレート関数を用いて、例外オブジェクトeから取り出すことが出来ます。もし、指定したerror_infoがeに含まれていない場合は、NULLポインタが返されます。

実際のアプリケーションなら、ここで取得したエラーの情報から、エラー情報ログをとったり、ファイル名を含むエラー情報をダイアログに表示したりして、よりアプリケーションにふさわしいエラーハンドリングが出来ます。

また、Boost.Exceptionには、boost::exceptionクラスの例外オブジェクトが保持しているエラー情報を簡単に文字列にして返すboost::diagnostic_informationという関数があります。これは自動生成なので、ユーザーに対するメッセージとしてはあまり読み易くないですが、debugやloggingには便利です。

おわりに

Boost.Exception、特にその使用方法における、例外のthrowとcatchについて見てきました。
ここに書いてあることはBoost.Exceptionのドキュメントに全て書いてあります。
Boost.Exceptionにはこの他、boost::exceptionから派生していないクラスを用いている例外機構をboost::exceptionに統合する機能やマルチスレッドアプリケーション上で例外を扱う方法(N2179を元にしている)をサポートしています。


今回調べていて、Boost.Exceptionって思っていたより良さそうに感じたんですが、誰か使っている人がいたら使い心地とかおせーてくだしあ。


というわけで、次の担当者 筑波のプロのBjammer(id:Flast)に向けてthrow;