私はC++98を使っているプロジェクトに参加している。タイトルを見て「2017年にもなってC++98だとォ〜〜」というような感想を抱くかもしれないが、ライブラリとしてC++98をサポートしたいとか、C++98時代から作っているプログラムでメジャーバージョンアップのタイミングでもないのに切り替えるのはちょっと、とか、共用サーバーにいつまで経ってもC++11対応のコンパイラが入らず泣く泣くg++4.2とかを使わされている(野良ビルドしろよ、というのを全ユーザーに強いるのは酷だと思うのだ)などの理由でC++98はまだまだ現役である。いいか悪いかは別として。
でも我々には強い味方がいる。Boostだ。Boostさえあれば、BOOST_AUTO
とauto
など多少使い勝手は違えど、ほとんどありとあらゆるC++の新機能が使える。それどころでなく、標準ライブラリに未だ採用されていない非常に便利な機能がBoostには大量に入っている。variant
もその一つだ。C++17からstd::variant
が入ったが、それをすでにバリバリ使える状況になっている人は少数派だろう。そもそもGCCやClangなどで(確かC++17コア言語は済んでいたはずだが)ライブラリサポートはどれくらい進んでいるのだろうか? 実務でならC++14を使えるだけで凄まじく恵まれた職場だと言えると思う。
variant
は「型安全なunion
」とでもいうべきものだ。Rustをやっている人にはenum
ですと言えば伝わるのだろうか。ユーザーが指定した有限種類の型のうちのどれか一つの値を格納し、ジェネリックまたはオーバーロードによるvisitor
によって明示的なキャストなしに値へアクセスできる。継承やBoost.Any
と比較した時の最も大きな魅力の一つは、variant
ならスタックに乗るということだ。知っての通り、ヒープメモリのアロケーションは非常にコストが高い。また、ポインタのデリファレンスもコストが高く、特にメモリが断片化されてキャッシュミスが増えるとまずい。いちいちnew
してポインタを介してアクセスするよりも、variant
の方が早くなる可能性は十分にある。値にアクセスする機能がしっかりサポートされているのも良い。
で、BoostのおかげでこいつがC++98でも使える。当然バンバン使っているのだが、唯一面倒なのがいちいちvisitor
を書かねばならないことだ。意外とこれがリーダビリティを損ねてしまう。
とりあえずほぼ無意味なvisitorを作ってみる。
struct some_visitor : boost::static_visitor<void> { some_visitor(){} result_type operator()(int i){std::cout << i << std::endl; return;} result_type operator()(double d){std::cout << d << std::endl; return;} }; boost::variant<int, double> v1(3.14), v2(42); boost::apply_visitor(some_visitor(), v1); // 3.14 boost::apply_visitor(some_visitor(), v2); // 42
これはまだシンプルでわかりやすい例だが、variant
を初めて見た場合は「??」となるかもしれない。
また、プライベート変数にvariant
を使っていて、場合によっては他のプライベート変数を変更する操作をしたい場合、friend struct
にするか、クラス内struct
のような結構奇妙なことをしないといけなくなる。
class X { public: void do_something() { private_visitor pv(*this); boost::apply_visitor(pv, this->v_); } private: struct private_visitor : boost::static_visitor<void> { private_visitor(X& x) : x_(x){} void operator()(int i) {x_.hoge(i);} void operator()(double d) {x_.fuga(d);} X& x_; }; void hoge(int); void fuga(double); private: boost::variant<int, double> v_; };
なんだこれは。もう少しいい方法がないものだろうか。しかし流石に現代、C++11やC++14でvisitor
をうまくやる的なものは見つかったが、C++98のことはみんな忘れてしまったようだ。良いことだ。しかし私は困る。
どうしたものか。以下のようなことができたら私は文句ないのだが。
void print(int i) {std::cout << i << std::endl;} void print(double d) {std::cout << std::showpoint << d << std::endl;} boost::variant<int, double> v(3.14); boost::apply_visitor(make_visitor(resolve<int>(print), resolve<double>(print)), v);
ここでオーバーロード解決のためにresolve
という関数を噛ませているがこのあたりはあとで適当に変えていく。
という訳で作ろう。すでにできているのだが今から作るていの方が話が進めやすい。
一番簡単な場合、つまりvariant
は2つの型しか持たず、よってvisitor
も二つあれば十分、とする。また、関数は全て引数は一つだけと想定する。ここで、以前見たmake_overloadやcppreferenceにあるstruct overload
のことを思い出し、以下のようなものを書いた。
template<typename T0, typename T1> struct aggregate_functions2 : T0, T1 { typedef typename T0::result_type result_type; aggregate_functions2(T0 t0, T1 t1): T0(t0), T1(t1){} using T0::operator(); using T1::operator(); };
variadic templateが使えたらもっと汎用性高くできるのだが、C++98にそんな便利なものはないのでスルー。この状態でもファンクタを受け取ればうまくいくのだが、目標は関数ポインタだったのでこれでは終わらない。まず、継承でき、result_type
を取り出せる状態にしたい。
result_type
を取り出す一番「C++らしい」やり方は、function_traits
のようなものを作って関数ポインタやその他に対し特殊化することだろう。とりあえずこんな感じで作る。
template<typename T> struct function_traits { typedef typename T::result_type result_type; }; template<typename R, typename T> struct function_tratis<R(*)(T)> { typedef R result_type; };
ちょっと待ってごめん今検索したらこれそのまんまBoostにあるわ function_traits - 1.41.0
はい。という訳でこんなものを作る必要はないです。
(6/14追記:これはファンクタには使えない感じっぽいのでやっぱりこういうクラスは要る、ただしファンクタのcall-operatorオーバーロードなどの可能性を考慮しないことになるのでdetail名前空間に入れてしまう方がよさそう)
気を取り直して、関数ポインタをそのまま継承するわけにいかないのでラッパークラスを作る、のではなくboost::function
を使う。一応、boost::function
は呼ぶ度に空かどうかのチェックとかをしていた気がするので、そのオーバーヘッドが気になるなら自分で粗雑で危険な簡素なラッパーを作ってもいいのかもしれないが、まあ今回は面倒なのでやめておく。
また、オーバーロードされている関数は名前だけでは型が決まらず、static_cast
とかによってオーバーロード解決をやる必要があるので、そこを一気にやってしまおう。以下のようなヘルパ関数を作る。引数の宣言方法に注意だ。Cをゴリゴリ書いている人はお馴染みかもしれないが、C++をやっているとあまり見ない。
template<typename R, typename T> inline boost::function<R(T)> resolve(R(*fptr)(T)) { return boost::function<R(T)>(fptr); }
この関数をテンプレート引数付きで呼べば、その関数ポインタへのキャストが行われ、オーバーロード解決ができるだろう(と思う)。よく考えてみると、キャストするまでポインタの値が決まらないというのは不思議な挙動な気がするが、まあ今はいい。ところで、Boost.Variantにはvisitor_ptr
というものがあり、これは関数ポインタをvisitor
にしてくれるというものだ。ここのboost::function
はboost::visitor_ptr_t
でいい気もする。
で、あとはさっきのaggregate_function
を作るmake_visitor
関数を作ろう。
template<typename T0, typename T1> inline aggregate_function2<T0, T1> make_visitor(T0 t0, T1 t1) { return aggregate_function2<T0, T1>(t0, t1); }
よろしい。これで、以下のように書けるようになった。
void print(int i) {std::cout << i << std::endl;} void print(double d) {std::cout << std::showpoint << d << std::endl;} boost::variant<int, double> v; boost::apply_visitor(make_visitor( resolve<void, int>(print), resolve<void, double>(print)), v);
かなりスッキリした。また、ジェネリックな関数の場合、以下のようにできる。
template<typename T> void print(T t){std::cout << t << std::endl;} boost::variant<int, double> v; boost::apply_visitor(make_visitor( resolve(print<int>), resolve(print<double>)), v);
この場合resolve
関数テンプレートの型推論が発動し、そちらの型指定は不要になる。返り値の型を書かなくて良い分楽だ。
次はメンバ関数だ。関数のメンバ関数もポインタで持つことができるが、一つだけ落とし穴がある。メンバ関数はthis
ポインタを暗黙の裡に引数として取っているということだ。呼ぶべきオブジェクトが決まっているなら(大抵thisだろう)、最初に作る時にbind
してしまえばいい。メンバ関数用にresolve
関数をオーバーロードする。
template<typename R, typename T, typename C> inline boost::function<R(T)> resolve(R(C::*fptr)(T), C* cptr) { return boost::function<R(T)>(boost::bind(fptr, cptr, _1)); }
これの返り値の型も単純にboost::bind
の返り値そのものにしようかと思ったのだけどC++98での作法がわからん(auto f(arg) -> decltype(g(arg))とやりたい)。
これで、以下のようなことができる。
class X { public: X(int i) : v(i){} X(double d): v(d){} void invoke() { return boost::apply_visitor(make_visitor( resolve(&X::hoge, this), resolve(&X::fuga, this)), this->v); } private: void hoge(int); void fuga(double); private: boost::variant<int, double> v; };
メンバ関数のアドレスを取得する時にどうしても少しだけ面倒になるが、それでも最初に書いていたクラス内visitor
よりも格段にシンプルになった。
もしvariadic templateが使えれば最初に任意個のaggregate_function
を書けたのだが、C++98なのでそうはいかない。しかし、関数2つ用のmake_visitor
しかないのは使い勝手が悪すぎる。となると、2個から10個までくらいのaggregate_functionN
とmake_visitor
のオーバーロードを用意するのが一番手っ取り早い(Boostも一部そうやっているところがあった気がする)。力ずくで全部書いてもいいが、汎用性に欠けるのと、単調でつまらないというのがある。こういう場合の解決策は、マクロだ。
次回はプリプロセッサマクロを使って、先に定義しておいた個数までmake_visitor
を展開する。明日ブログ書く余裕あるかな……