この記事は、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さんにバトンタッチ！