c++98でもvariantを(バリバリ)使いたい! その1

私はC++98を使っているプロジェクトに参加している。タイトルを見て「2017年にもなってC++98だとォ〜〜」というような感想を抱くかもしれないが、ライブラリとしてC++98をサポートしたいとか、C++98時代から作っているプログラムでメジャーバージョンアップのタイミングでもないのに切り替えるのはちょっと、とか、共用サーバーにいつまで経ってもC++11対応のコンパイラが入らず泣く泣くg++4.2とかを使わされている(野良ビルドしろよ、というのを全ユーザーに強いるのは酷だと思うのだ)などの理由でC++98はまだまだ現役である。いいか悪いかは別として。

でも我々には強い味方がいる。Boostだ。Boostさえあれば、BOOST_AUTOautoなど多少使い勝手は違えど、ほとんどありとあらゆる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_overloadcppreferenceにある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::functionboost::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_functionNmake_visitorオーバーロードを用意するのが一番手っ取り早い(Boostも一部そうやっているところがあった気がする)。力ずくで全部書いてもいいが、汎用性に欠けるのと、単調でつまらないというのがある。こういう場合の解決策は、マクロだ。

次回はプリプロセッサマクロを使って、先に定義しておいた個数までmake_visitorを展開する。明日ブログ書く余裕あるかな……