【C++ Advent Calendar 2012】Boost.TypeErasure【2日目(後編)】

C++ Advent Calendar 2012 2日目の後編の記事になります。


前編はこちら。

[宣伝]

不定期ですが、オンライン上で読書会を開いています。


気になる方はぜひ参加してみてはどうでしょうか!

概要

Boost.TypeErasure の布教を簡単に。

C++ での一般的なポリモフィズム

C++ポリモフィズムを実現する場合、一般的には継承を使用して次のような感じになるかと思います。

#include <iostream>
#include <memory>
#include <vector>

struct animal{
    void
    virtual say() const = 0;

    virtual ~animal(){}
};

struct inu : animal{
    void
    virtual say() const override{
        std::cout << "(U^ω^)わんわんお!" << std::endl;
    }
};

struct tori : animal{
    void
    virtual say() const override{
        std::cout << "ヾ ゚∋゚)ノシ クペー クペー" << std::endl;
    }
};


int
main(){
    std::vector<std::shared_ptr<animal>> animals;
    animals.push_back(std::make_shared<inu>());
    animals.push_back(std::make_shared<tori>());

    for(auto&& a : animals){
        a->say();
    }

    return 0;
}
/*
output:
(U^ω^)わんわんお!
ヾ ゚∋゚)ノシ クペー クペー
*/


animal クラスをインターフェースとして使用し、継承側のクラスで処理を実装という割りと一般的な継承の使い方ですね。
しかし、このように継承を使用した場合では animal クラスを継承したクラスオブジェクトしか保持できないという欠点があります。

struct neko{
    void
    virtual say() const override{
        std::cout << "(「・ω・)「にゃー" << std::endl;
    }
};

// animal クラスを継承してないけどクラスオブジェクトを保持したい!!
animals.push_back(std::make_shared<neko>());


この欠点を緩和して使用することが出来るのが Boost.TypeErasure になります。

Boost.TypeErasure とは

Boost.TypeErasure とは継承に依存しないで定義したコンセプトを満たすクラスオブジェクトを保持することが出来るライブラリです。
また、コンセプトを通じて保持しているクラスオブジェクトを簡単に操作することも出来ます。
既存のライブラリでは Boost.Any や Boost.Function がそれに近いかと思います。


Boost.TypeErasure は採用されることは決まりましたが、まだ正式な Boostのライブラリではない事に注意して下さい。
(その為、下記で提示したコードは正式にリリースされた場合に変更されている可能性があります。

Boost.TypeErasure を使ってみる

言葉にするとちょっとわかりづらいかと思うので実際に書いてみましょう。
簡単に使用してみるとこんな感じになります。

#include <boost/type_erasure/any_cast.hpp>
#include <boost/type_erasure/operators.hpp>
#include <boost/type_erasure/operators.hpp>
#include <boost/mpl/vector.hpp>
#include <iostream>
#include <string>
#include <vector>

struct X{
    int value;
};

X
operator +(X const& a, X const& b){
    return X{ a.value + b.value };
}

std::ostream&
operator <<(std::ostream& os, X const& x){
    return os << x.value;
}

int
main(){
    namespace te = boost::type_erasure;
    
    // boost::type_erasure::any が保持するクラスオブジェクトのコンセプトを決める
    typedef boost::mpl::vector<
        // 加算コンセプト
        te::addable<>,
        // std::ostream への出力コンセプト
        te::ostreamable<>,
        // コピーコンストラクタコンセプト
        te::copy_constructible<>
    > requirements;

    // requirements で定義されたコンセプトを満たすクラスオブジェクトを保持できる
    typedef te::any<requirements> any;

    any hoge{12};
    std::cout << hoge << std::endl;
    std::cout << hoge + hoge << std::endl;


    std::vector<any> objects;

    // any へ暗黙的に型変換出来ないので emplace_back を使用
    objects.emplace_back(3.14f);
    objects.emplace_back(std::string("homu"));
    objects.emplace_back(X{42});

    // requirements を満たしていないのでエラー
//  objects.emplace_back({1, 2, 3});

    for(auto&& object : objects){
        std::cout << object << std::endl;
        std::cout << object + object << std::endl;
    }
    
    return 0;
}
/*
output:
12
24
3.14
6.28
homu
homuhomu
42
84
*/


コード見てもらえればなんとなく何をやっているのかがわかると思います。
最初に保持するクラスオブジェクトのコンセプトを決めて、そのコンセプトを満たすクラスオブジェクトを保持することが出来ます。
もちろん、コンセプトを満たさないクラスオブジェクトを保持しようとすると コンパイルエラーとなります。
指定できるコンセプトは Boost.TypeErasure 内いくつか用意されているのでそれを使用することが出来ます。


また、クラスオブジェクトに要求するコンセプトはユーザ側でも定義することが出来ます(下記に続く。

ユーザ側でコンセプトを定義する

先ほどの継承を使用したポリモフィズムは下記のように書くことが出来ます。
クラスオブジェクトに定義されているメンバ関数を要求するのがポイントですね。

#include <boost/type_erasure/any_cast.hpp>
#include <boost/type_erasure/member.hpp>
#include <boost/mpl/vector.hpp>
#include <iostream>
#include <string>
#include <vector>


// say メンバ関数を持っているかどうかをチェックするコンセプトを定義する
BOOST_TYPE_ERASURE_MEMBER((has_say), say, 0)


// say メンバ関数が存在すれば animal オブジェクトである
typedef boost::type_erasure::any<
    boost::mpl::vector<
        // コンセプト
        has_say<void()>,
        boost::type_erasure::copy_constructible<>
    >
> animal;


// 継承が必要なく各クラスに say メンバ関数を定義するだけ
struct inu{
    void
    say() const{
        std::cout << "(U^ω^)わんわんお!" << std::endl;
    }
};

struct tori{
    void
    say() const{
        std::cout << "ヾ ゚∋゚)ノシ クペー クペー" << std::endl;
    }
};

struct neko{
    void
    say() const{
        std::cout << "(「・ω・)「にゃー" << std::endl;
    }
};


int
main(){
    
    std::vector<animal> animals;
    
    // say メンバ関数を持っているクラスであればなんでも保持することが出来る
    animals.emplace_back(inu{});
    animals.emplace_back(tori{});
    animals.emplace_back(neko{});

    for(auto&& a : animals){
        a.say();
    }

    return 0;
}
/*
output:
(U^ω^)わんわんお!
ヾ ゚∋゚)ノシ クペー クペー
(「・ω・)「にゃー
*/


継承も仮想関数も必要がないので実装側のクラスはだいぶすっきりした書き方になっていますね。

まとめ

簡単ですが Boost.TypeErasure について紹介してみました。
継承を使用しないでポリモフィズムが実現出来るのは大きいと思います。
コンセプトを変更することで柔軟に対応できますしね。
今回はポリモフィズムの例だけでしたが他にもまだ利用できそうな点はあるかと思います。
個人的にはこれが結構便利なんじゃな以下と思っています。
まだ、trunk にすら入っていませんが、Boost.TypeErasure が正式にリリースされるのが楽しみですね!!

次回予告

明日の C++ Advent Calendar 2012id:Suikaba さんです。
Boost.Contract はわたしも気になっているので楽しみですね!!