読者です 読者をやめる 読者になる 読者になる

boost::variant の visitor にラムダ式を使う

boost::variant は複数の方を入れられる型安全な union のようなコンテナで,次のように visitor パターンを使ってアクセスできます.

Boost逆引きリファレンス > ユーザー定義型を扱える型安全な共用体

ですが,ちょっとした要素アクセスのために毎度 visitor を定義するのは面倒ですし,テンプレートメンバを持つクラスはローカルに定義できないのでグローバルに定義しなければいけないこともあります. 昨日の GREE Tech Talk #5 の懇親会でめるぽんさんとアキラさんとそんな話をしていたときにめるぽんさんが「C++14 になってジェネリックラムダが入ったんで余裕ですよ!」みたいな感じのことをおっしゃっていて,便利そうなので boost::apply_visitor() の代わりにラムダ式を渡せる apply_lambda() という関数を作ってみました.

こんな感じに使えます:

#include <string>
#include <boost/variant/variant.hpp>
#include "apply_lambda.hpp"

// 整数型か小数点数型を表す
using Number = boost::variant<int, double>;

int main()
{
    Number n1 = 42;
    Number n2 = 3.14;

    // 文字列化してみる
    std::string s1 = apply_lambda([](auto const& n){ return std::to_string(n); }, n1); // "42"
    std::string s2 = apply_lambda([](auto const& n){ return std::to_string(n); }, n2); // "3.14"

    // 2引数バージョンで足してみる
    auto added = apply_lambda([](auto const& l, auto const& r){ return l + r; }, n1, n2); // 45.14
}

もちろん外の変数をキャプチャしたり,Boost.Lambda も使えます.

実装ですが,boost::variant の visitor は boost::static_visitor<結果の型> を継承しないといけない(内部的には,visit したときの戻り値型を知るために,visitor のメンバに result_type というメンバ型を持たないといけない)ため,単純にラムダ式を渡すことは出来ません.

なので,渡されたラムダ式から decltype() を使って visit した際の戻り値型を推論し,ラムダ式boost::static_visitor<推論した戻り値型> の両方を多重継承する wrapper をつくり,それを boost::apply_visitor() で適用することで実現します.

ソースはこんな感じ.

#include <boost/variant/variant.hpp>
#include <boost/variant/static_visitor.hpp>
#include <boost/variant/apply_visitor.hpp>

namespace detail {

template<class Lambda, class Result>
struct lambda_wrapped_visitor
    : public boost::static_visitor<Result>
    , public Lambda {

    lambda_wrapped_visitor(Lambda const& l) noexcept
        : Lambda(l)
    {}

};
} // namespace detail

template<class Lambda, class Head, class... Tail>
inline auto apply_lambda(Lambda const& l, boost::variant<Head, Tail...> const& variant)
{
    return boost::apply_visitor(detail::lambda_wrapped_visitor<Lambda, decltype(std::declval<Lambda>()(std::declval<Head const&>()))>{l}, variant);
}

template<class Lambda, class Head, class... Tail>
inline auto apply_lambda(Lambda const& l, boost::variant<Head, Tail...> &variant)
{
    return boost::apply_visitor(detail::lambda_wrapped_visitor<Lambda, decltype(std::declval<Lambda>()(std::declval<Head &>()))>{l}, variant);
}

template<class Lambda, class Head, class... Tail, class Head2, class... Tail2>
inline auto apply_lambda(Lambda const& l, boost::variant<Head, Tail...> const& variant, boost::variant<Head2, Tail2...> const& variant2)
{
    return boost::apply_visitor(detail::lambda_wrapped_visitor<Lambda, decltype(std::declval<Lambda>()(std::declval<Head const&>(), std::declval<Head2 const&>()))>{l}, variant, variant2);
}

template<class Lambda, class Head, class... Tail, class Head2, class... Tail2>
inline auto apply_lambda(Lambda const& l, boost::variant<Head, Tail...> const& variant, boost::variant<Head2, Tail2...> &variant2)
{
    return boost::apply_visitor(detail::lambda_wrapped_visitor<Lambda, decltype(std::declval<Lambda>()(std::declval<Head &>(), std::declval<Head2>()))>{l}, variant, variant2);
}

これでちょっとした処理はわざわざ visitor をクラスで定義しなくともラムダ式で書けるようになりました.さっそく犬言語をリファクタリング中…

このままだと boost::variant の中の要素にジェネリックにしかアクセスできませんが,複数のラムダ式オーバーロードされた関数オブジェクトをつくる術があるようなので,そういうのを使うとちゃんと型で分岐することもできます.

ここは函:make_overloadを書いた

ただ,実装が大きくなるので使いどころは考えないといけないと思います.