この記事は、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
Background
、Character
、Effect
という3つのクラスにはもはや継承関係はありません。
update()
メンバ関数という共通インタフェースを持ってるだけのクラス群です。
update()
の呼び出しは、boost::variant
のビジターを使用して行っています。
new
とunique_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さんにバトンタッチ!