ごあいさつ
こんにちは。meviy形状処理チームの志田です。
弊社では数年前にRustが導入され、その適用範囲は徐々に拡大しています。 (Rust導入の経緯についてはこちらの記事をご覧ください)
しかし現在でも、C++を書く必要がある場面は少なくありません。そんな時、Rustを触り始めて便利さを実感している私としては「Rustのあの機能がC++でも使えたらなあ...」と感じることがよくあります。
特に強く感じるのが、パターンマッチ構文の恩恵です。
現状、C++にはパターンマッチ構文がないため、std::variantの分岐処理をRustのように簡潔に記述できません。
そこで今回は、C++でもパターンマッチのような書き方を実現する方法をご紹介したいと思います。
std::variant の分岐
まず、if文を使った素直な分岐処理を見てみましょう。 以下のようなコードになりますが、型チェックが冗長で可読性に欠けます。
std::variant<int, std::string> var = 42; std::string type_str; if (std::holds_alternative<int>(var)) { type_str = "integer"; } else if (std::holds_alternative<std::string>(var)) { type_str = "string"; }
std::visitを使えばif文を使わずに分岐処理できます。しかし、毎回structで関数オブジェクトを定義する必要があり、これもまた冗長に感じられます。
// 関数オブジェクトを使う場合 std::variant<int, std::string> var = 42; struct visitor { std::string operator()(int i) { return "integer"; } std::string operator()(const std::string& str) { return "string"; } }; const auto type_str = std::visit(visitor{}, var);
C++でもパターンマッチ(のようなもの)
それでは本題に入ります。
前述したstructで関数オブジェクトを定義する方法は動作しますが、もう少し簡潔に書きたいです。
そこで、ラムダ式の型を多重継承する手法を使います。
これにより、複数のラムダ式から1つの関数オブジェクトを生成し、std::visitに渡すことができるようになります。
具体的な実装は下記のようになります。
// 複数のラムダ式を多重継承で統合 template<class... Ts> struct overload : Ts... { using Ts::operator()...; }; // 推論補助 template<class... Ts> overload(Ts...) -> overload<Ts...>; // ラムダ式の多重継承を使ったmatchの実装 template<typename Variant, typename... Functors> constexpr auto match(Variant&& variant, Functors&&... functors) { return std::visit( overload{ std::forward<Functors>(functors)... }, std::forward<Variant>(variant) ); }
このmatch関数を使うと、以下のような書き方ができます。
struct定義が不要になり、スッキリと書けるようになりました。
std::variant<int, std::string> var = 42; const auto type_str = match( var, [](int i) { return "integer"; }, [](const std::string& str) { return "string"; } );
"のようなもの"
ただし、この手法にはパターンが網羅されていない場合でもコンパイルが通ってしまう、デフォルトハンドラがない、where句に相当する機能がないなどの制約があります。
Rustのパターンマッチほど柔軟ではありません。あくまで"のようなもの"です。
それでもC++でstd::variantの分岐処理をよりスッキリと書けるようになります。
Rustが恋しい今日このごろです。