コンセプトを使用しないコンセプト : Traits

BoostCon '11で「Boost.Generic: Concepts without Concepts」というセッションがあるらしいので内容を予想してみる。一つはConceptCheckがあると思いますが、今回はTraitsによるConcept MapとConcept-based Overloadを解説していこうと思います。


Boostでは近年、Fusion, Polygon, Geometryといったライブラリが、Traitsと呼ばれる手法によってConcept MapとConcept-based Overloadを表現しています。
たとえば、コンセプトベース線形代数ライブラリの設計を考えてみましょう。ベクトルクラスは通常以下のように定義することになりますが

template <class T>
struct vector2 {
    T x, y;
};

コンセプトベースなライブラリの場合には、これをアダプト可能なインタフェースとしてのベクトルクラスを定義します。

namespace la {
    template <class T>
    struct vector_traits {
        typedef typename T::value_type value_type;

        static value_type x(const T& p) { return p.x; }
        static value_type y(const T& p) { return p.y; }

        static T construct(value_type x_, value_type y_)
            { return T(x_, y_); }
    };
} // namespace la

さらに、ベクトルに対するアルゴリズムを、このTraitsで設計されたベクトルクラスを通して定義します。

namespace la {
    template <class T, class Vector1, class Vector2>
    T inner_product(const Vector1& v, const Vector2& u)
    {
        return vector_traits<Vector1>::x(v) * vector_traits<Vector2>::x(u) +
               vector_traits<Vector1>::y(v) * vector_traits<Vector2>::y(u);
    }
} // namespace la

こうすることによって、vector_traitsにアダプトされた全てのベクトルクラスに対して、同じアルゴリズムを適用することができるようになります。

struct Vector {
    float x, y;
    Vector(float x_, float y_) : x(x_), y(y_) {}
};

namespace la {
    template <>
    struct vector_traits<Vector> {
        typedef float value_type;

        static value_type x(const Vector& p) { return p.x; }
        static value_type y(const Vector& p) { return p.y; }

        static Vector construct(value_type x_, value_type y_)
            { return Vector(x_, y_); }
    };
} // la

const Vector v(1, 1);
const Vector u(3, 5);

const float result = la::inner_product<float>(v, u);

これによって、他の線形代数のライブラリのベクトルクラス、どこにでもあるPointクラスといったもの全てに対して、同じアルゴリズムを適用できるようになり、アルゴリズムは誰か一人が書けばいい、というような他のライブラリまでをも視野に入れた汎用的な設計になるのです。


また、演算子の定義や、(ADLによって)同名の関数が起こり得るケース、コンパイルエラーメッセージの改善といったケースにおいて、vector_traitsにアダプトされた型でオーバーロードしたい、という要求が稀によく出てきます。そういった要求のために、アダプトされているかどうかを判定するメタ関数を別途用意し、特殊化する際にはvector_traitsと判定用のメタ関数の両方を特殊化してもらうガイドラインにしておくと後々便利です。


以下は、先ほどの内積を求めるアルゴリズム関数に、vector_traitsをアダプトしたと表明した型でなければ即座にエラーにする、という機能を追加し、Concept-based Overloadの例として出力ストリームを追加しています。

namespace la {

    template <class T>
    struct is_vector_type : boost::mpl::false_ {};

    template <class T, class Vector1, class Vector2>
    T inner_product(const Vector1& v, const Vector2& u)
    {
        BOOST_STATIC_ASSERT(is_vector_type<Vector1>::value);
        BOOST_STATIC_ASSERT(is_vector_type<Vector2>::value);

        return vector_traits<Vector1>::x(v) * vector_traits<Vector2>::x(u) +
               vector_traits<Vector1>::y(v) * vector_traits<Vector2>::y(u);
    }

    namespace io {
        template <class Vector>
        typename boost::enable_if<is_vector_type<Vector>, std::ostream&>::type
            operator<<(std::ostream& os, const Vector& v)
        {
            os << '(' << vector_traits<Vector>::x(v) << ','
                      << vector_traits<Vector>::y(v) << ')';
            return os;
        }
    } // namespace io

} // namespace la

...

namespace la {
    template <>
    struct is_vector_type<Vector> : boost::mpl::true_ {};
}

...

const Vector v(1, 1);
const Vector u(3, 5);

const float result = la::inner_product<float>(v, u);
std::cout << result << std::endl;

using namespace la::io;
std::cout << v << std::endl;

これが、近年Boostで取り入れられているコンセプトベースライブラリの設計です。


以下、本エントリの完全なソースコードです。

#include <iostream>
#include <boost/mpl/bool.hpp>
#include <boost/static_assert.hpp>
#include <boost/utility/enable_if.hpp>

namespace la {
    template <class T>
    struct vector_traits {
        typedef typename T::value_type value_type;

        static value_type x(const T& p) { return p.x; }
        static value_type y(const T& p) { return p.y; }

        static T construct(value_type x_, value_type y_)
            { return T(x_, y_); }
    };

    template <class T>
    struct is_vector_type : boost::mpl::false_ {};

    template <class T, class Vector1, class Vector2>
    T inner_product(const Vector1& v, const Vector2& u)
    {
        BOOST_STATIC_ASSERT(is_vector_type<Vector1>::value);
        BOOST_STATIC_ASSERT(is_vector_type<Vector2>::value);

        return vector_traits<Vector1>::x(v) * vector_traits<Vector2>::x(u) +
               vector_traits<Vector1>::y(v) * vector_traits<Vector2>::y(u);
    }

    namespace io {
        template <class Vector>
        typename boost::enable_if<is_vector_type<Vector>, std::ostream&>::type
            operator<<(std::ostream& os, const Vector& v)
        {
            os << '(' << vector_traits<Vector>::x(v) << ','
                      << vector_traits<Vector>::y(v) << ')';
            return os;
        }
    } // namespace io

} // namespace la

struct Vector {
    float x, y;
    Vector(float x_, float y_) : x(x_), y(y_) {}
};

namespace la {
    template <>
    struct vector_traits<Vector> {
        typedef float value_type;

        static value_type x(const Vector& p) { return p.x; }
        static value_type y(const Vector& p) { return p.y; }

        static Vector construct(value_type x_, value_type y_)
            { return Vector(x_, y_); }
    };

    template <>
    struct is_vector_type<Vector> : boost::mpl::true_ {};
} // la

int main()
{
    const Vector v(1, 1);
    const Vector u(3, 5);

    const float result = la::inner_product<float>(v, u);
    std::cout << result << std::endl;

    using namespace la::io;
    std::cout << v << std::endl;
}
8
(1,1)