Boost.Variantで継承を置き換える

C++

この記事は、C++ Advent Calendar 2013の参加記事です。


今回は、継承を使ったプログラムを、Boost.Variantで置き換えてみよう、というチャレンジ記事です。
まず、継承を使った単純なプログラムを用意します。

update()メンバ関数という共通インタフェースを持ったクラスのオブジェクトをリストとして持つ、というよくあるプログラムです。

#include <iostream>
#include <memory>
#include <vector>
#include <boost/range/adaptor/indirected.hpp>

struct UpdateInterface {
    virtual void update() = 0;
};

struct Background : public UpdateInterface {
    void update() override
    { std::cout << "background" << std::endl; }
};

struct Character : public UpdateInterface {
    void update() override
    { std::cout << "character" << std::endl; }
};

struct Effect : public UpdateInterface {
    void update() override
    { std::cout << "effect" << std::endl; }
};

class TaskList {
    std::vector<std::unique_ptr<UpdateInterface>> taskList_;
public:
    void update()
    {
        for (UpdateInterface& x : taskList_ | boost::adaptors::indirected) {
            x.update();
        }
    }

    template <class Task, class... Args>
    void add(Args... args)
    {
        taskList_.push_back(std::unique_ptr<UpdateInterface>(new Task(args...)));
    }
};

int main()
{
    TaskList task;
    task.add<Background>();
    task.add<Character>();
    task.add<Effect>();

    task.update();
}
background
character
effect

このプログラムの改善したい点:

  • 継承をなくしたい
  • インタフェース合わせのためだけのnewをなくしたい

ではこれを、Boost.Variantに置き換えてみましょう。

#include <iostream>
#include <vector>
#include <boost/variant.hpp>

struct Background {
    void update()
    { std::cout << "background" << std::endl; }
};

struct Character {
    void update()
    { std::cout << "character" << std::endl; }
};

struct Effect {
    void update()
    { std::cout << "effect" << std::endl; }
};

struct UpdateVisitor {
    using result_type = void;

    template <class T>
    void operator()(T& x)
    { x.update(); }
};

class TaskList {
    using TaskType = boost::variant<Background, Character, Effect>;
    std::vector<TaskType> taskList_;
public:
    void update()
    {
        for (TaskType& x : taskList_) {
            UpdateVisitor vis;
            boost::apply_visitor(vis, x);
        }
    }

    template <class Task, class... Args>
    void add(Args... args)
    {
        taskList_.push_back(Task(args...));
    }
};

int main()
{
    TaskList task;
    task.add<Background>();
    task.add<Character>();
    task.add<Effect>();

    task.update();
}
background
character
effect

BackgroundCharacterEffectという3つのクラスにはもはや継承関係はありません。
update()メンバ関数という共通インタフェースを持ってるだけのクラス群です。


update()の呼び出しは、boost::variantのビジターを使用して行っています。
newunique_ptrの代わりにboost::variantを使用するようになったので、インタフェース合わせのためにフリーストアは一切使用しません。全てスタックで処理されます。


これのイケてないところは、update()関数を呼び出すためだけに、ビジタークラスを定義しなければならないところです。これをなんとかして、ラムダ式に置き換えたいです。


しかし、Boost.LambdaもC++11のラムダ式も、メンバ関数呼び出しに対しては単相なので、具体的な型がわかっていなければなりません。
そこでC++14のジェネリックラムダ(多相ラムダ)です。


この記事の最終目標です。
boost::variantの共通インタフェース呼び出しをC++14ラムダ式で書けるようにしてみましょう!

#include <iostream>
#include <vector>
#include <boost/variant.hpp>

template <class R, class F>
class function_wrapper {
    F f_;
public:
    using result_type = R;

    function_wrapper(F f)
        : f_(f) {}

    template <class T>
    result_type operator()(T& x)
    {
        return f_(x);
    }

    template <class T>
    result_type operator()(const T& x)
    {
        return f_(x);
    }
};

template <class R, class F>
function_wrapper<R, F> wrap(F f)
{
    return function_wrapper<R, F>(f);
}


struct Background {
    void update()
    { std::cout << "background" << std::endl; }
};

struct Character {
    void update()
    { std::cout << "character" << std::endl; }
};

struct Effect {
    void update()
    { std::cout << "effect" << std::endl; }
};

class TaskList {
    using TaskType = boost::variant<Background, Character, Effect>;
    std::vector<TaskType> taskList_;
public:
    void update()
    {
        for (TaskType& x : taskList_) {
            auto f = wrap<void>([](auto& task) { task.update(); });
            boost::apply_visitor(f, x);
        }
    }

    template <class Task, class... Args>
    void add(Args... args)
    {
        taskList_.push_back(Task(args...));
    }
};

int main()
{
    TaskList task;
    task.add<Background>();
    task.add<Character>();
    task.add<Effect>();

    task.update();
}
background
character
effect

できました!
具体的には、この部分です。

auto f = wrap<void>([](auto& task) { task.update(); });
boost::apply_visitor(f, x);

共通インタフェース呼び出しのためのビジターを、ラムダ式で書けるようになりました。


wrap()という関数は、ラムダ式に戻り値の型result_typeを持たせるためのラッパー関数です。apply_visitor()に渡すビジターは、共通の戻り値の型であるresult_typeを持っている必要があるため、これを書きました(result_ofには対応していない)。


継承をboost::variantで置き換えるやり方は、使用する型のリストが事前にわかっている場合に使用できます。
こういう設計をするなら、実はBoost.TypeErasureを使えばいいのですが、ラムダ式でビジターを書いてみたかったのでやってみました。


2日目の5mingame2さんにバトンタッチ!