Egg の楽しみ

これは Boost Advent Calendar 2011 の参加記事 (11 日目)です.
今日はせっかくの機会なので,個人的に関心の高い Egg について,ちょっと書いてみたいと思います.Egg を知らない人も多いと思いますので,ここでは入門・解説を書くのではなく,どのようなライブラリなのかをゆるく紹介するような記事にしたいと思います.コード例もありますが,軽く流して雰囲気を感じてください.

Egg とは

id:mb2sync さんの作成された,関数オブジェクトを作るためのライブラリです.
「関数オブジェクトって operator() をオーバーロードするだけで作れるんじゃないの?」と思われた方,全くもってその通りです.しかし前規格 C++03 では,式の型を求める汎用的な方法 (decltype) がなかったため,operator() の戻り値の型を求めるための仕組みを,operator() とは別に用意しておく必要がありました.その仕組みの作成をお膳立てしてくれるのが,Egg に含まれる egg::function です.本稿の最初の方で説明します.
それ以外にも Egg には,関数オブジェクトを作るための機能がいくつもあります.これらは,新しく関数オブジェクトを作る Function Builders,既存の関数オブジェクトを元に関数オブジェクトを作る Function Adaptors,有名な関数テンプレートを関数オブジェクトにした Function Objects に分けられます.本稿の真ん中くらいで紹介します.
本稿の最後の方では,C++11 での Egg について,私の考えるところを書きます.

2 つの Egg

Egg には,P-Stade C++ Libraries に含まれるものと,Boost に Boost.Egg として提案されたものとがありますが,基本的な部分は同じです.以降のコード例では Boost.Egg を使用しますが,BOOST_EGG_* となっているマクロを PSTADE_EGG_* に読み替えれば P-Stade でもそのまま使えます.インストールする方法は各リンク先を参照してください.

egg::function

では始めましょう.一から関数オブジェクトを作るときの強力なヘルパー,egg::function から紹介します.
先程戻り値の型がどうこう書きましたが,改めて説明します.C++03 では,(boost::result_of によって) 戻り値の型を求めることができる関数オブジェクトを書くのは実に面倒でした.それを示すために,引数への参照をそのまま返す関数オブジェクト identity を C++03 で書いてみます.

// これに相当するものを書く
/*
template <class T>
inline T &identity(T &t) // lvalue
{
    return t;
}

template <class T>
inline T const &identity(T const &t) // rvalue or const lvalue
{
    return t;
}
*/

struct T_identity
{
    template <class FunCall>
    struct result;

    template <class Fun, class A1>
    struct result<Fun(A1 &)>
    {
        typedef A1 &type;
    };

    template <class Fun, class A1>
    struct result<Fun(A1)>
    {
        typedef A1 const &type;
    };

    template <class A1>
    A1 &operator()(A1 &a1) const
    {
        return a1;
    }

    template <class A1>
    A1 const &operator()(A1 const &a1) const
    {
        return a1;
    }
};

T_identity const identity = {};

// boost::result_of で戻り値の型を求められる
BOOST_MPL_ASSERT((boost::is_same<
    boost::result_of<T_identity(int)>::type, int const &
>));

なんか面倒な感じが分かってもらえれば十分です.もうちょっと踏み込むと,クラステンプレート result を宣言して特殊化を 2 つ定義しているところ (ここで戻り値の型を定義します),operator() を 2 つ定義しているところ,そしてその operator() で改めて戻り値の型を書いているところがいかにも冗長です.
egg::function はこの辺をうまく解決します.

struct little_identity
{
    template <class Me, class A1>
    struct apply
    {
        typedef A1 &type;
    };

    template <class Re, class A1>
    Re call(A1 &a1) const
    {
        return a1;
    }
};

typedef function<little_identity> T_identity;
T_identity const identity = {{}};

一気に簡単になりました.クラステンプレート apply の定義を 1 つ,メンバ関数テンプレート call の定義を 1 つ書くだけで済んでおり,call の戻り値の型もテンプレート引数で Re とだけ書いておけば十分です.
これはかなり単純な例ですが,もっと複雑な関数オブジェクトを書こうとすると,egg::function によって省かれる労力はかなり大きくなります.C++03 で関数オブジェクトを書くときに少しでも面倒だと感じたら,Egg の利用を考慮すると良いでしょう.詳しい使い方はリファレンスを参照してください.

ちょっと寄り道:boost::forward_adapter

Egg がない場合,boost::forward_adapter が役に立つかもしれません.上の例だと,

struct imperfect_identity
{
    template <class FunCall>
    struct result;

    template <class Fun, class A1>
    struct result<Fun(A1 &)>
    {
        typedef A1 &type;
    };

    template <class A1>
    A1 &operator()(A1 &a1) const
    {
        return a1;
    }
};

typedef boost::forward_adapter<imperfect_identity> T_identity;
T_identity const identity;

となります.operator() の戻り値の型については egg::function ほど簡単にはなりませんが,Boost に入っているので気軽に使えると思います.

Function Builders

ピッチを上げて次々に Egg の機能を紹介していきます.面白いと思うものがあれば遊んでみましょう.
Function Builders は,新しい関数オブジェクトを作る機能です.先ほどの egg::function もここに含まれます.
ここでは generator を紹介しましょう.generator を使って作られた関数オブジェクトは,引数の型と値を元に新しいオブジェクトを生み出します.次の例は,make_pair を作るものです.

typedef
    generator<
        std::pair<
	    deduce<boost::mpl::_1, as_value>,
	    deduce<boost::mpl::_2, as_value>
	>
    >::type
T_make_pair;

T_make_pair const make_pair = BOOST_EGG_GENERATOR();

void egg_example()
{
    BOOST_CHECK(make_pair(42, 3.14) == std::make_pair(42, 3.14));
}

Function Adaptors

既存の関数オブジェクトから新しい関数オブジェクトを生み出す機能です.関数合成,カリー化,不動点コンビネータ (さらにメモ化) など,関数型言語が好きな人なら気に入りそうな機能が盛りだくさんです.カリー化の例を示しましょう.

struct base_my_plus
{
    typedef int result_type;

    int operator()(int x, int y, int z) const
    {
        return x + y + z;
    }
};

result_of_curry3<base_my_plus>::type const curried_my_plus = BOOST_EGG_CURRY3({});

void egg_example()
{
    BOOST_CHECK(curried_my_plus(1)(2)(3) == 6);
}

Oven の Range Adaptors の実装に使われている pipable もここにあります.拡張メソッドライクなものを作る場合に役立つでしょう.例は,Function Builder の implicit との合わせ技で T t = "文字列" | parse; を可能にするものです.

template <class To>
struct X_lexical_cast
{
    typedef To result_type;

    template <class From>
    To operator()(From const &from) const
    {
        return boost::lexical_cast<To>(from);
    }
};

typedef
    result_of_pipable<
    	implicit<X_lexical_cast<boost::mpl::_1> >::type
    >::type
T_parse;

T_parse const parse = BOOST_EGG_PIPABLE(BOOST_EGG_IMPLICIT());

void egg_example()
{
    int const i = "42" | parse;
    BOOST_CHECK(i == 42);
}
また寄り道:static initialization

関数オブジェクトはヘッダに書いておくと便利です.しかし,ヘッダで名前空間スコープにオブジェクトを定義した場合,一般には ODR と初期化の順序が問題になります.
ODR については,関数オブジェクトを const にしておけばほとんど問題は生じないと考えられます.まじめに考え出すと泥沼なので (参照 1参照 2),そういうことにしておきましょう.
さて,初期化の順序ですが,ローカルでない静的オブジェクトの初期化の順番は一般には定まりません (Effective C++ 4 項など).しかしある種の初期化は,順序を気にせずに (その他の種類の初期化より前に) 行われることが保証されており,static initialization と呼ばれます (3.6.2).具体的には,最初のゼロ初期化と定数式での初期化がこれに当たります.例えば,次のオブジェクト a の初期化は static initialization です.

struct A
{
    int i;
};

A const a = { 42 };

Static initialization は,初期化順の問題を解決すると同時に,実行時のオーバーヘッドを減らす効果も期待されます.そこで Egg は,static initialization になる初期化子をマクロで提供しています.これまでの例に登場した BOOST_EGG_GENERATOR() などの奇妙なマクロがそうで,BOOST_EGG_GENERATOR() は {} に展開されます.
C++11 では定数式の範囲が広がったため,これらのマクロは不要になると考えられます.

Function Objects

最後に紹介する機能は,有名な関数テンプレートを関数オブジェクトにした Function Objects です.ここでは boost::get を元にした get を紹介しましょう.これは boost::tuple だけでなく,あらゆる Fusion 列から指定した位置の要素を抜き出します.身近なところでは,std::pair の最初の要素を取る関数オブジェクトを boost::mem_fn(&std::pair<first_type, second_type>::first) から X_get_c<0>() に変えるなどできます.

void egg_example()
{
    using phx::arg_names::arg1;
    using phx::arg_names::arg2;

    std::vector<std::pair<int, std::string> > adc =
        boost::assign::pair_list_of
        (3, "hotwatermorning")(1, "cpp_akira")(2, "kikairoya")(4, "Flast_RO");

    // 最初の要素でソートする
    boost::sort(adc, phx::bind(X_get_c<0>(), arg1) < phx::bind(X_get_c<0>(), arg2));
}

C++11 での Egg

C++11 では,関数オブジェクトを書くのがとても楽になりました.ほとんどの場合,operator() を定義しておけばそれで十分で,しかも Perfect Forwarding のためにずらずらとオーバーロードを並べる必要もありません.最初の identity を書き直してみましょう.

struct T_identity
{
    template <class T>
    constexpr T &&operator()(T &&t) noexcept
    {
        return std::forward<T>(t);
    }
};

constexpr T_identity identity = {};

とても簡単になりました.C++11 では,egg::function の必要性は薄れていくと思われます.しかし Egg には本稿で紹介したような興味深い機能が他にいくつもあり,それらは C++11 でも有用です.これらは内部で boost::result_of を使っているので,BOOST_RESULT_OF_USE_DECLTYPE マクロを定義しておくと幸せになれるでしょう.
さて,C++11 流の Perfect Forwarding を求める人や,「全部 constexpr でやりたいんだ!」というアレな先進的な人には多少の不満が残るかもしれません.そこで実験がてら,C++11 で Egg を書き直してみました.
https://github.com/iorate/Egg
GCC 4.7.0 の -std=c++11 を前提に書かれています.また,C++03 の制限に由来すると思われる機能はばっさり切り捨てています.使用例を示しましょう.

struct base_my_plus
{
    template <class T, class U>
    constexpr auto operator()(T &&t, U &&u)
        -> decltype(std::forward<T>(t) + std::forward<U>(u))
    {
        return std::forward<T>(t) + std::forward<U>(u);
    }
};

constexpr auto my_plus = pipable(base_my_plus());

static_assert((1 | my_plus(2)) == 3, "");

result_of_* や奇妙な初期化子マクロが消え,またコンパイル時実行をサポートしている点に注目してください.

実は寄り道の方が本編:Forwarding Strategies

C++03 で Perfect Forwarding を実装するのはものすごく大変でした (参照 3.「簡単でしょ?」と思った人は魔クロに毒されています).書くのが面倒なだけならともかく,サポートする引数の数を増やすとコンパイル時間が爆発する危険もありました.
そこで Egg では,perfect ではないが軽量な forwarding 戦略をいくつか提供しています.たとえば by_cref は,引数を const 参照で転送します.

struct little_size
{
    template <class Me, class Range>
    struct apply
      : boost::range_difference<Range>
    {};

    template <class Re, class Range>
    Re call(Range const &rng) const
    {
        return boost::size(rng);
    }
};

typedef function<little_size, by_cref> T_size;
T_size const size = {{}};

C++11 ではこれらの戦略の必要性はあんまりないと考えたため,上の Egg もどきでは Perfect Forwarding のみをサポートしています.

おわりに

ずいぶん長くなりましたが,C++03 で一から関数オブジェクトを書くときは egg::function が便利で,それ以外にも面白い関数オブジェクトを作る機能がいくつもあるよ,という話でした.
またインターフェース,実装ともに大変よく考えられていて勉強になるので,ライブラリ寄りな人は読んでみると面白いと思います.
明日は id:gintenlabo さんによる Boost.Chrono の紹介です.