読者です 読者をやめる 読者になる 読者になる

Boost.Contract の出力先を変更する

C++ Boost

あけましておめでとうございます。今年もよろしくお願いします。

さて、新年一発目の記事はタイトルにあるとおりです。
そのコードが以下になります。

//main.cc
#include <contract.hpp>
#include <boost/exception/all.hpp>

#include <iostream>
#include <fstream>

class my_exception
    : public boost::exception,
      public std::exception
{};

CONTRACT_FUNCTION(
    void (func) ( const int arg )
    precondition( arg >= 0 ? true : throw my_exception() ) // 違反なら例外を投げる
)
{
    // ...
}
 
void throwing_handler( const contract::from& context )
{
    // ここでは出力先をファイルにしています
    const std::string log_file_path( "./contract_log.txt" );
    std::ofstream ofs( log_file_path );

    try
    {
        // 投げられている例外(裏で溜まってる?)をcatchさせる
        throw;
    }
    catch(contract::broken& failure) // contract の assertion で 
                                     // false になればこれが投げられる
    {
         ofs << failure.file_name() << "(" << failure.line_number() <<
                "): contract assertion \"" << failure.assertion_code() <<
                "\" failed " << std::endl;
    }
    catch(std::exception& failure) // ユーザーがstd::exceptionを継承して作った例外
                                   // (example. my_exception)とか、
                                   // 標準の例外(example. std::bad_alloc)
                                   // が投げられたらこっち
    {
        ofs << "contract failed: " << failure.what() << std::endl;
    }
    catch(...) // その他
    {
        ofs << "contract failed with unknown error" << std::endl;
    }

    // デストラクタからの例外は投げない!
    // すなわち、std::terminateとはおさらばさ!
    if( context == contract::FROM_DESTRUCTOR )
    {
        ofs << "exception from destructor!";
    }
    else
    {
        // re-throwすることで、先ほどのアクティブな例外を伝播させる
        throw;
    }
}

int main()
{
    // precondition と postcondition でのアサーションを
    //  捉える関数をthrowing_handlerに設定する
    // ほかにも、contract::set_class_invariant_broken, 
    //           contract::set_block_invariant_broken
    //  等がある
    contract::set_precondition_broken( &throwing_handler );
    contract::set_postcondition_broken( &throwing_handler );

    try
    {
        func( -1.0 );
    }
    catch( const std::exception& e ) // throwing_handlerの最後で
                                     // re-throwされたのがここでcatchされる
    {
        // こっちは標準エラー出力
        std::cerr << e.what() << std::endl;
    }
}

ちなみに、contract::brokenはstd::logic_errorを継承していて、std::exceptionで捉えることが可能です。

標準のthrowing_handlerにあたるdefault_broken_handlerという関数は、デストラクタから例外が投げられたら常にstd::terminateを呼び出してます。ユーザーがset_precondition_broken等を呼びだし、上記のようなハンドリングをすることでそれを防ぐこともできます。

余談ですが、僕はこんな感じで書いてます。

void throwing_handler( const contract::from& context )
{
    if( context == contract::FROM_DESTRUCTOR )
    {
        //...
    }
    else
    {
        throw;
    }
}

int WINAPI _tWinMain( HINSTANCE, HINSTANCE, LPTSTR, int )
{
    try
    {
        contract::set_precondition_broken( &throwing_handler );
        contract::set_postcondition_broken( &throwing_handler );

        //...
    }
    catch( const boost::exception& e )
    {
        //...
    }
    catch( const std::exception& e )
    {
        //...
    }
    catch( ... )
    {
        //...
    }
    return 0;
}

throwing_handlerがえらくすっきりしていますが、Main関数のほうで大きくtryで囲ってるので、ちゃんと例外が拾えます。
この書き方だと、contract以外でMainまで漏れてきた例外を全部catchできるので、結構好きなんですが、どうなんでしょうか。