C++/標準ライブラリ/variant
はじめに
[編集]variant
ヘッダーでは、std::variant
型が定義されています。
std::variant
型は、異なる型の値を一つずつ保持できる型安全な多様体(discriminated union)です。C++に標準で用意されていたunion型は、型安全性が欠けていましたが、std::variant
型はその欠点を解消しています。
monostate
は、空の状態を表す特別な型で、variant
の代替型として使われます。
variantクラステンプレート
[編集]コンストラクタ
[編集]- デフォルトコンストラクタで空の状態で初期化できます
- コピー・ムーブコンストラクタがあります
T&&
値から直接構築できるコンストラクタがありますin_place_type_t
、in_place_index_t
を使った直接生成もできます
デストラクタ
[編集]デストラクタで、variant
が保持していた値が適切に破棄されます。
代入演算子
[編集]コピー代入とムーブ代入、およびT&&
値による直接代入ができます。
emplace関数
[編集]emplace
を使うと、イン場合構築でvariant
に値を設定できます。型インデックスか型を直接指定します。
状態観測関数
[編集]valueless_by_exception
で空の状態かどうかを判定でき、index
で現在のインデックス(型)を取得できます。
スワップ
[編集]swap
関数で、2つのvariant
オブジェクトを入れ替えられます。
補助クラステンプレート・別名テンプレート
[編集]variant_size
、variant_size_v
は、variant
の代替型の数を取得するトレイトです。variant_alternative
、variant_alternative_t
は、インデックスから代替型を取得します。
値アクセス関数
[編集]holds_alternative
[編集]holds_alternative
は、variant
が指定した型の値を保持しているかを判定します。
get
[編集]get
は、インデックスか型を指定して、variant
内の値へアクセスします。型が一意でない場合はエラーになります。
get_if
[編集]get_if
は、get
に似ていますが、アクセスに失敗した場合はヌルポインタを返します。
比較演算子
[編集]variant
には、==
!=
<
>
<=
>=
<=>
などの比較演算子が多数定義されています。内部の値同士を比較し、型が異なる場合は型ごとに総序定義されています。
monostate
同士の比較にも、==
と<=>
が定義されています。
visitation
[編集]visit
関数は、variant
の内部の値に基づいて、指定した関数オブジェクトまたはラムダを呼び出す手段を提供します。これにより、union風の分岐処理が型安全に書けます。
例外クラス
[編集]bad_variant_access
は、variant
への不正なアクセスが行われた場合にスローされる例外クラスです。
補助機能
[編集]std::swap
にvariant
の特殊化があり、効率的なスワップが可能です。std::hash
にもvariant
用の特殊化が用意されています。
使用例
[編集]variant
は、従来のunionの代替として利用できます。ただし、型安全性が高く、メタデータも扱えるので、unionよりはるかに安全で生産的です。
visitation
を使えば、visitorパターンを安全に実装でき、実行時の型に基づいて処理を分岐できます。
// unionの代替としてのvariant union Value { int int_val; double double_val; std::string str_val; }; // 上のunionを安全に扱うのは難しい // variantを使った例 std::variant<int, double, std::string> value; value = 42; // int value = 3.14; // double value = "hello"s; // std::string // 型安全にアクセス可能 if (std::holds_alternative<int>(value)) { std::cout << "int: " << std::get<int>(value) << std::endl; } else if (std::holds_alternative<double>(value)) { std::cout << "double: " << std::get<double>(value) << std::endl; } else if (std::holds_alternative<std::string>(value)) { std::cout << "string: " << std::get<std::string>(value) << std::endl; }
この例では、variant
を使うことで、intとdoubleとstd::stringの値を安全に保持できています。アクセスする際もget
関数で型を指定するので、型安全性が保たれます。
次に、visitation
の使用例を示します。
std::variant<int, double, std::string> get_value() { // ...値を取得する処理... return /* ... */; } void handle_value(const std::variant<int, double, std::string>& value) { std::visit([](const auto& val) { using T = std::decay_t<decltype(val)>; if constexpr (std::is_same_v<T, int>) { std::cout << "int: " << val << std::endl; } else if constexpr (std::is_same_v<T, double>) { std::cout << "double: " << val << std::endl; } else if constexpr (std::is_same_v<T, std::string>) { std::cout << "string: " << val << std::endl; } }, value); } // ... auto value = get_value(); handle_value(value);
この例では、get_value
関数がvariant
を返し、handle_value
関数がそのvariant
の値に応じた処理をstd::visit
で行っています。ラムダ式の中では、テンプレート引数の型T
からvariant
の代替型を判別し、型に応じた処理を行っています。
visitation
を使えば、実行時の型に基づく分岐処理を、unionのようにたくさんのtypeid
チェックを書く必要なく、型安全に記述できます。
std::any
との違いstd::variant
とstd::any
はどちらも実行時の型の値を扱うための標準ライブラリの型ですが、いくつか大きな違いがあります。
std::variant
- 保持できる型をコンパイル時に列挙する必要がある
- 格納されている値の型がコンパイル時に分かる(つまり、型安全)
- 適切な代替型に基づいた効率的な実装が可能
- 代替型に対するメタデータ(インデックスなど)にアクセスできる
- visitation機能により、実行時の型に基づいた処理が可能
std::any
- 実行時に任意の型の値を格納できる
- 格納されている値の型は実行時でしか分からない(つまり、型安全ではない)
- 内部実装は型エラーズされているため、非効率
- メタデータへのアクセスはない
- visitation機能がない
つまり、std::variant
は静的に型が決まっており、それに応じた最適化や機能が提供される一方、std::any
は実行時に任意の型を扱えるがそれに伴う制約があります。
std::variant
を使う場合は、保持する型を事前に列挙できるような状況で、型安全性や最適化が必要とされます。一方、std::any
はその制約がなく、柔軟に任意の型を扱う必要がある場合に使われます。
std::variant
の方が効率的で型安全なコードが書けますが、std::any
の方が柔軟性は高くなります。用途に応じて適切な型を選択する必要があります。variantの実装
[編集]variant
は内部で、格納している値の型に対応するインデックスと、その値へのポインタ(またはその値自体の広げ開いた表現)を保持しています。
まとめ
[編集]std::variant
型は、型安全な多様体としてunionを置き換えるものです。variantは、ペイロードの型に応じた多数の演算、visitation、高い型安全性など、unionにはない様々な機能を持っています。実行時の値の型に応じた処理が必要な場面で、variantは強力なツールとなります。