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 が返ってきている
}

実際の実装部分でインクルードしているのは optionaltype_traitsvariant だけです

実装の説明

gets<T>(a)T 型の値を std::variant 型の変数 a から取得できます

図にしてみると、今回の例はちょうど木構造になっています

f:id:matumoto_h:20220119224715p:plain

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 文だと、コンパイル時に条件を見てくれなくなり多くのエラーが生まれます(再帰が止まらないなど)

あとがき

もう少しうまい実装ができそうな気もするのですが、今回は思いつきませんでした

またなにか妙案など思いついたら再実装してみたく思います