std::variant で再帰的に保持している値を取得する
はい、題名が若干怪しい日本語になってますが気にしないで下さい
やりたいこと
using V1 = std::variant<int, std::string>; using V2 = std::variant<char, V1>; using V3 = std::variant<double, V2>; int main() { V3 a = "aiueo"; // ここで std::get<std::string>(a); で std::string 型の値("aiueo") を取得したい }
上のコードのように、std::variant
がネストしている場合に、何回も std::get
を繰り返すのはめんどくさいです
もし、愚直にやるとしたら std::get<std::string>(std::get<V1>(std::get<V2>(a)));
のようにする必要があるかと思います
本当に保持しているかも確認したい場合は std::holds_alternative
を使う必要も出てきます
そこで、gets<T>(a)
で std::variant
型の変数 a
から T
型の値を抜き出すものがあれば便利です
実装
ということで、作ってみました
#include <optional> #include <type_traits> #include <variant> #include <string> #include <iostream> template <typename T> struct is_variant: std::false_type {}; template <typename... Args> struct is_variant<std::variant<Args...>>: std::true_type {}; template <typename T> inline constexpr bool is_variant_v = is_variant<T>::value; template <typename T, typename Variant, size_t i = std::variant_size_v<Variant> - 1> constexpr std::optional<T> gets(Variant); template <typename T, typename Variant, size_t i> constexpr std::optional<T> gets_helper(Variant a) { using i_th_type = std::variant_alternative_t<i, Variant>; if (not std::holds_alternative<i_th_type>(a)) return std::nullopt; if constexpr (std::is_same_v<T, i_th_type>) return std::optional(std::get<T>(a)); if constexpr (is_variant_v<i_th_type>) return gets<T, i_th_type>(std::get<i_th_type>(a)); else return std::nullopt; } template <typename T, typename Variant, size_t i = std::variant_size_v<Variant> - 1> constexpr std::optional<T> gets(Variant a) { static_assert(0 <= i and i < std::variant_size_v<Variant>); if constexpr (i == 0) { return gets_helper<T, Variant, i>(a); } else { std::optional<T> res1 = gets_helper<T, Variant, i>(a); std::optional<T> res2 = gets<T, Variant, i - 1>(a); if (res1) return res1; if (res2) return res2; return std::nullopt; } } using V1 = std::variant<int, std::string>; using V2 = std::variant<char, V1>; using V3 = std::variant<double, V2>; int main() { V3 a = "aiueo"; std::cout << gets<std::string>(a).value() << std::endl; // std::cout << gets<int>(a).value() << std::endl; エラー! std::nullopt が返ってきている }
実際の実装部分でインクルードしているのは optional
、type_traits
、variant
だけです
実装の説明
gets<T>(a)
で T
型の値を std::variant
型の変数 a
から取得できます
図にしてみると、今回の例はちょうど木構造になっています
gets
関数でしていることは、引数で与えられた節点の子をすべて探索することです
それとは別に、gets_helper
関数でしていることは、引数で与えられた節点が std::variant
型であれば探索する深さを一段深くして gets
で探索し直すことです
これによって、再帰的に型を辿って探索していきます
自問自答
簡単な Q&A をまとめてみました
なぜ返り値の型が
std::optional<T>
なのか
無効な値を返すときにoptional
以外に思いつかなかったからなぜ
constexpr
関数なのか
constexpr
にできそうだったからなぜ concept などを使っていないのか
筆者がちゃんとした使い方をまだ理解しきれていないからなぜ i がテンプレート引数で、それをデクリメントするように再帰しているのか
これは for 文を使ってないのは何故かという意味ですが、constexpr
関数の都合上 for 文は使えないからif (not std::holds_alternative<i_th_type>(a)) return std::nullopt;
はなぜif constexpr
じゃないのか
仮引数a
が式中に存在し、明示的なコンパイル時計算ができないからelse いらないだろ
なんとなくでつけちゃいましたなぜ
if constexpr
を使用しているのか
コンパイル時に条件を見てくれるようにするためです
constexpr
がないif
文だと、コンパイル時に条件を見てくれなくなり多くのエラーが生まれます(再帰が止まらないなど)
あとがき
もう少しうまい実装ができそうな気もするのですが、今回は思いつきませんでした
またなにか妙案など思いついたら再実装してみたく思います