にゃははー

はへらー

便利かもしれないGCC builtin

これは恐らくC++ Advent Calendar 2014の15日目です。

古来[要出典]より人々はいかに例外を投げた奴をトレースするかに命をかけてきた[要出典]

投げられた例外自体や、そのメッセージを確認することはできるが、一体誰がその例外を投げたかという情報は一切乗らないからだ。
いっそのことsegfaultでもしてくれたほうがデバッガでスタックトレースを出力できる。

ある人は考えるだろう。

template <typename Base>
struct exception_info : Base
{
    explicit
    exception_info(const Base &base, const char *file, int line)
      : Base(base), file(file), line(line) {}

    virtual ~exception_info() noexcept {}

    const char *file;
    int line;
};

template <typename E>
[[noreturn]] inline void
throw_(const E &e, const char *file = __FILE__, int line = __LINE__)
{
    throw exception_info<E>(e, file, line);
}

{
    throw_(std::runtime_error("hogefuga"));
}

残念。__FILE__と__LINE__はプリプロセッサで展開されてしまうので、throw_がある行とファイルが例外に乗って飛んでくる。

正解は

template <typename E>
[[noreturn]] inline void
throw_(const E &e, const char *file, int line)
{
    throw exception_info<E>(e, file, line);
}

#define THROW(e) throw_(e, __FILE__, __LINE__)

{
    THROW(std::runtime_error("hogefuga"));
}

さて、関数名もほしい。C++11なら__func__が使える。GCCなら__FUNCTION__(場合によっては__PRETTY_FUNCTION__かもしれない)だ。
catchする側は今度受け取った例外オブジェクトがexception_infoから派生しているか確認して、fileとか取れるなら表示したい。RTTIで分岐するしかない。

とまぁ、ここまで来ると正直Boost使いましょう*1*2ってなる。なれ。

#include <boost/throw_exception.hpp>
#include <boost/exception/diagnostic_information.hpp>

{
    BOOST_THROW_EXCEPTION(std::runtime_error("Boooooooooooooost"));
}

catch (std::exception &e)
{
    std::cout << boost::diagnostic_information(e) << std::endl;
}

まぁでも、結局Boostもだいたい同じようなことをやっているので、エラー食らった後にいろいろやって、結果、例外を投げる的な共通関数をつくった時にはいい感じにBoostも使えない(そのいい感じの共通関数を呼んだ奴が結局誰かわからない)。
関数呼び出しもマクロで囲めば良いんだけど何でもかんでもマクロなのはほげふが〜ってなってしまうので、もうちょっとスマートな方法を探したい。

そんな時にはGCC 4.9以降*3*4で使える次のbuiltin関数*5を使うと良いかもしれない。

  • __builtin_LINE()
  • __builtin_FILE()
  • __builtin_FUNCTION()

それぞれ、__LINE__、__FILE__と__func__に対応している。

これらは関数であるのでプリプロセッサで展開はされないし、デフォルト引数として使えば呼んだ奴のコンテキストで評価される*6

実際に使ってみるとこうなる。
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

しかし自分でlineとか扱うとなると、先のBOOST_THROW_EXCEPTIONは使えないので、boost::throw_exceptionを呼ぶようにちょっと加工してやる。

template <typename E>
[[noreturn]] inline void
throw_(E &&ex, std::tuple<const char *, int, const char *> caller_ctx = std::make_tuple(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION()))
{
    boost::throw_exception(
      boost::enable_error_info(ex)
        << boost::throw_function(std::get<2>(caller_ctx))
        << boost::throw_file(std::get<0>(caller_ctx))
        << boost::throw_line(std::get<1>(caller_ctx)));
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

これでnamespace下にthrow_を置いたり出来るし、もうちょっといろいろな関数を経由したりするときにはcaller_ctxを持ちまわってやれば良い。


私は更にbacktraceを乗せたりして使っているのでなにか参考になれば幸いだ。
shinano/exception.hpp at develop · Flast/shinano · GitHub
shinano/exception.cpp at develop · Flast/shinano · GitHub

あ、ちなみにclangは実装してないから。m9m9m9

*1:boost exception - 1.57.0

*2:boost/throw_exception.hpp - 1.57.0

*3:実際には4.8から実装されているが、C++への対応が不十分で使い物にならない

*4:Bug 59821 – __builtin_LINE and __builtin_FILE for new'd objects is wrong

*5:Other Builtins - Using the GNU Compiler Collection (GCC)

*6:C++のデフォルト引数はcaller contextで評価されることが規定されている