C++ で一般化された on を書く

Data.Function

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
on f g = \x y -> f (g x) (g y)

これの引数の数と型を一般化したものを,C++ を使っていくつかの方法で書いてみました.Variadic な関数オブジェクトを書く上で参考になるかもしれません.
まず C++0x の機能を使ったものから.

#include <utility>
#include <type_traits>

namespace on_detail {

    template <class F, class G>
    struct result
    {
        F m_f;
        G m_g;

        // sorry, unimplemented: use of 'type_pack_expansion' in template
        /*
        template <class ...Args>
        auto operator()(Args &&...args) const ->
            decltype(m_f(m_g(std::forward<Args>(args))...))
        {
            return m_f(m_g(std::forward<Args>(args))...);
        }
        */

        template <class A, class = void>
        struct result_of_g
        {};

        template <class A>
        struct result_of_g<A, decltype(std::declval<G const &>()(std::declval<A>()), void())>
        {
            typedef decltype(std::declval<G const &>()(std::declval<A>())) type;
        };

        template <class ...Args>
        auto operator()(Args &&...args) const ->
            decltype(m_f(std::declval<typename result_of_g<Args>::type>()...))
        {
            return m_f(m_g(std::forward<Args>(args))...);
        }
    };

}

template <class F, class G> inline
auto on(F &&f, G &&g) ->
    on_detail::result<typename std::decay<F>::type, typename std::decay<G>::type>
{
    return { std::forward<F>(f), std::forward<G>(g) };
}

g++ の "sorry, unimplemented" に引っかかったので回り道をしていますが,本質的には variadic templates を使って仕様を書き下しているだけです.C++0x の素晴らしさが伝わってきます.
次に C++03 での実装を考えましょう.可変長引数は,オーバーロードを並べ立てることでエミュレートできます.そう,Boost.PP です.また,関数オブジェクトを作る際には,Boost.Egg が非常に有用です.これらのライブラリを贅沢に使って実装します.

#include <boost/utility/result_of.hpp>
#include <boost/preprocessor/repetition/enum_binary_params.hpp>
#include <boost/preprocessor/repetition/enum_params.hpp>
#include <boost/preprocessor/repetition/enum.hpp>
#include <boost/preprocessor/arithmetic/inc.hpp>
#include <boost/preprocessor/facilities/intercept.hpp>
#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/iteration/local.hpp>
#include <boost/egg/function.hpp>
#include <boost/egg/generator.hpp>
#include <boost/egg/construct_braced2.hpp>
#include <boost/mpl/placeholders.hpp>

namespace on_detail {

    template <class F, class G>
    struct little_result
    {
        F m_f;
        G m_g;

        typedef typename boost::result_of<F const()>::type nullary_result_type;

        template <class Re>
        Re call() const
        {
            return m_f();
        }

        template <class Me, BOOST_PP_ENUM_BINARY_PARAMS(BOOST_PP_INC(BOOST_EGG_MAX_ARITY), class A, = void BOOST_PP_INTERCEPT)>
        struct apply;

#define ON_apply_g_to_args(z, i, unused) typename boost::result_of<G const(BOOST_PP_CAT(A, i) &)>::type
#define ON_call_g_with_args(z, i, unused) m_g(BOOST_PP_CAT(a, i))
#define BOOST_PP_LOCAL_MACRO(n) \
        template <class Me, BOOST_PP_ENUM_PARAMS(n, class A)> \
        struct apply<Me, BOOST_PP_ENUM_PARAMS(n, A)> \
          : boost::result_of<F const(BOOST_PP_ENUM(n, ON_apply_g_to_args, ~))> \
        {}; \
        template <class Re, BOOST_PP_ENUM_PARAMS(n, class A)> \
        Re call(BOOST_PP_ENUM_BINARY_PARAMS(n, A, &a)) const \
        { \
            return m_f(BOOST_PP_ENUM(n, ON_call_g_with_args, ~)); \
        } \
        /**/
#define BOOST_PP_LOCAL_LIMITS (1, BOOST_EGG_MAX_ARITY)
#include BOOST_PP_LOCAL_ITERATE()
#undef ON_call_g_with_args
#undef ON_apply_g_to_args
    };

}

template <class F, class G>
struct result_of_on
{
    typedef boost::egg::function<on_detail::little_result<F, G> > type;
};

typedef
    boost::egg::generator<
        result_of_on<
            boost::egg::deduce<boost::mpl::_1, boost::egg::as_value>,
            boost::egg::deduce<boost::mpl::_2, boost::egg::as_value>
        >::type,
        boost::use_default,
        boost::egg::X_construct_braced2<>
    >::type
T_on;

T_on const on = BOOST_EGG_GENERATOR();

コードの説明はしません.ただし,variadic な関数オブジェクトを書く際にはしばしば,引数を取らない場合の戻り値に関する問題が発生し (参照),Egg はその良い解決策となることを指摘しておきます.
参考までに,この on を中置記法で使う例を紹介しておきましょう.

#include <vector>
#include <utility>
#include <string>
#include <boost/range/algorithm/sort.hpp>
#include <boost/egg/infix.hpp>
#include <boost/egg/functional.hpp>
#include <boost/egg/get.hpp>
#include "./on.hpp"

int main()
{
    namespace egg = boost::egg;
    using namespace boost::egg::infix;

    std::vector<std::pair<std::string, int> > v;

    // ...

    boost::sort(v, egg::less ^on^ egg::X_get_c<0>()); // pair の最初の要素でソートする
}

さて,上の実装は悪くありませんが,PP を使っているためデバッグしにくい,あるいは Boost の一部でない Egg を使っている,などの問題点もあります.そこで,Fusion を使った実装も示しておきましょう.

#include <boost/utility/result_of.hpp>
#include <boost/fusion/include/fused.hpp>
#include <boost/fusion/include/transform.hpp>
#include <boost/fusion/include/unfused.hpp>
#include <boost/type_traits/remove_reference.hpp>
#include <boost/functional/forward_adapter.hpp>

namespace on_detail {

    template <class F, class G>
    struct fused_result
    {
        F m_f;
        G m_g;

        fused_result(F f, G g)
          : m_f(f), m_g(g)
        {}

        template <class>
        struct result;

        template <class Me, class Args>
        struct result<Me(Args)>
          : boost::result_of<
                boost::fusion::fused<F>(
                    typename boost::fusion::result_of::transform<
                        typename boost::remove_reference<Args>::type,
                        G
                    >::type
                )
            >
        {};

        template <class Args>
        typename result<fused_result const(Args &)>::type operator()(Args &args) const
        {
            return boost::fusion::fused<F>(m_f)(boost::fusion::transform(args, m_g));
        }
    };

}

namespace result_of {

    template <class F, class G>
    struct on
    {
        typedef
            boost::forward_adapter<
                boost::fusion::unfused<on_detail::fused_result<F, G> >
            >
        type;
    };

}

template <class F, class G> inline
typename result_of::on<F, G>::type on(F f, G g)
{
    return boost::fusion::unfused<on_detail::fused_result<F, G> >(on_detail::fused_result<F, G>(f, g));
}

fused / unfused を使うことにより,複数の引数を受け取るコードではなく,それらをパックした 1 つの fusion シーケンスを受け取るコードを書いて実装することができます.ただし,unfused は lvalue しか受け取ることができないので,forward_adapter を挟んでやる必要があります.

感想

Egg が Boost に入らなかったのは C++ 七不思議の一つに数えても良いと改めて思いました (参照).