最終更新日時(UTC):
が更新

履歴 編集

function template
<variant>

std::visit(C++17)

namespace std {
  template <class Visitor, class... Variants>
  constexpr see below visit(Visitor&& vis, Variants&&... vars); // (1) C++17

  template <class R, class Visitor, class... Variants>
  constexpr R visit(Visitor&& vis, Variants&&... vars);         // (2) C++20
}

概要

variantオブジェクトが現在保持している型に対応する関数を呼び出す。

  • (1) : variantオブジェクトが現在保持している型に対応する関数を呼び出し、呼び出された関数の戻り値型で戻り値を返す
  • (2) : variantオブジェクトが現在保持している型に対応する関数を呼び出し、指定された戻り値型Rで戻り値を返す

switch文の自動化/Visitorパターン

variantオブジェクトに代入される型は実行時に決定される。そのため、通常の方法では、以下のようにswitch文を使用して実行時に代入された型に応じた処理を行うことになる:

void f(T1);
void f(T2);
void f(T3);

std::variant<T1, T2, T3> v;

switch (v.index()) {
  case 0:
    f(std::get<0>(v));
    break;

  case 1:
    f(std::get<1>(v));
    break;

  case 2:
    f(std::get<2>(v));
    break;
}

このvisit()関数を使用することで、そのような分岐を内部で解決してくれる。これは、GoFのデザインパターンのひとつであるVisitorパターンに対応する機能である。

注意点としては、非メンバ関数のオーバーロードを各型に対応した関数としてvisit()関数に指定することはできないということである。引数の型が静的に決まらなければ関数ポインタを取得できないため、関数ポインタではオーバーロードの集合をまとめてvisit()関数に渡すことができない。そのため、visit()関数に指定するオーバーロードの集合には、必然的に関数オブジェクトを使用することになる。

struct Visitor {
  void operator()(T1) {}
  void operator()(T2) {}
  void operator()(T3) {}
};

std::variant<T1, T2, T3> v;
std::visit(Visitor{}, v); // vに実行時に代入された型に応じた関数オーバーロードを呼び出す

呼び出された関数から戻り値を返す

戻り値を返す場合は、それぞれの関数オーバーロードに単に戻り値の型を定義すればよいが、全てのオーバーロードで共通の型でなければならない。これは、std::visit()関数自体の戻り値の型は静的に決定されなければならないためである。

struct Visitor {
  int operator()(T1) { return 0; }
  int operator()(T2) { return 1; }
  int operator()(T3) { return 2; }
};

std::variant<T1, T2, T3> v;

// vに実行時に代入された型に応じた関数オーバーロードを呼び出し、戻り値を受け取る
int result = std::visit(Visitor{}, v);

複数のvariantオブジェクトから呼び出す関数オーバーロードを決定する

複数のvariantオブジェクトが保持するそれぞれの値を引数として、対応する関数を呼び出すこともできる。その場合、とりうる全ての組み合わせをオーバーロードとして定義していなければならない。これは、オーバーロードを呼び出すインスタンス化はコンパイル時に行われるためである。

struct Visitor {
  void operator()(T1, T1) {}
  void operator()(T1, T2) {}
  void operator()(T1, T3) {}
  void operator()(T2, T1) {}
  void operator()(T2, T2) {}
  void operator()(T2, T3) {}
  void operator()(T3, T1) {}
  void operator()(T3, T2) {}
  void operator()(T3, T3) {}
};

std::variant<T1, T2, T3> arg1;
std::variant<T1, T2, T3> arg2;

// arg1とarg2に実行時に代入された型に応じた関数オーバーロードを呼び出す
std::visit(Visitor{}, arg1, arg2);

全ての候補型で共通の操作を行う場合は、ジェネリックラムダを使用できる

代入される型ごとにオーバーロードを定義できることは有用だが、すでに操作が共通化されていたり、共通の操作にアダプトできる場合 (GoFのデザインパターンのひとつであるAdapterパターンなど)、使用するビジター関数オブジェクトとして、ジェネリックラムダを使用できる。

std::variant<T1, T2, T3> v;

// T1, T2, T3のいずれの型が代入されたとしても、処理内容は共通
std::visit([](const auto& x) {
  std::cout << x << std::endl;
}, v);

適格要件

  • 全てのvars...に代入されたsize_t型インデックスのパックをmとする
  • (1) : 全てのmの組み合わせについて、式INVOKE(std::forward<Visitor>(vis), get<m>(std::forward<Variants>(vars))...)が適格であること
  • (2) : 全てのmの組み合わせについて、式INVOKE<R>(std::forward<Visitor>(vis), get<m>(std::forward<Variants>(vars))...)が適格であること

戻り値

  • 現在全てのvars...に代入されているsize_t型インデックスのパックをmとする
  • (1) : 式INVOKE(std::forward<Visitor>(vis), get<m>(std::forward<Variants>(vars))...)を呼び出して返す。戻り値の型は、その呼び出しの戻り値の型となる
  • (2) : 式INVOKE(std::forward<Visitor>(vis), get<m>(std::forward<Variants>(vars))...)を呼び出して返す

vars...が空で、ビジターのみが指定された場合、引数なしのオーバーロードを呼び出して返す。

例外

計算量

  • sizeof...(Variants)が1以下である場合、一度だけ対応する関数を呼び出す (定数時間)。そうでない場合、計算量は実装に要求しない

備考

#include <iostream>
#include <variant>
#include <string>

// 2倍にする。
// 整数だったら数値を2倍にし、
// 文字列だったら同じ文字列を2回繰り返す
struct TimesTwoVisitor {
  void operator()(int& n)
  {
    n *= 2;
  }

  void operator()(std::string& s)
  {
    s += s;
  }
};

int main()
{
  std::variant<int, std::string> v = 2;
  std::visit(TimesTwoVisitor{}, v);
  std::visit([](const auto& x) { std::cout << x << std::endl; }, v);

  v = std::string{"Hello"};
  std::visit(TimesTwoVisitor{}, v);
  std::visit([](const auto& x) { std::cout << x << std::endl; }, v);
}

出力

4
HelloHello

バージョン

言語

  • C++17

処理系

参照