コンテンツにスキップ

C++/標準ライブラリ/variant

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに[編集]

variantヘッダーでは、std::variant型が定義されています。 std::variant型は、異なる型の値を一つずつ保持できる型安全な多様体(discriminated union)です。C++に標準で用意されていたunion型は、型安全性が欠けていましたが、std::variant型はその欠点を解消しています。

monostateは、空の状態を表す特別な型で、variantの代替型として使われます。

variantクラステンプレート[編集]

コンストラクタ[編集]

  • デフォルトコンストラクタで空の状態で初期化できます
  • コピー・ムーブコンストラクタがあります
  • T&&値から直接構築できるコンストラクタがあります
  • in_place_type_tin_place_index_tを使った直接生成もできます

デストラクタ[編集]

デストラクタで、variantが保持していた値が適切に破棄されます。

代入演算子[編集]

コピー代入とムーブ代入、およびT&&値による直接代入ができます。

emplace関数[編集]

emplaceを使うと、イン場合構築でvariantに値を設定できます。型インデックスか型を直接指定します。

状態観測関数[編集]

valueless_by_exceptionで空の状態かどうかを判定でき、indexで現在のインデックス(型)を取得できます。

スワップ[編集]

swap関数で、2つのvariantオブジェクトを入れ替えられます。

補助クラステンプレート・別名テンプレート[編集]

variant_sizevariant_size_vは、variantの代替型の数を取得するトレイトです。variant_alternativevariant_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::swapvariantの特殊化があり、効率的なスワップが可能です。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::variantstd::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は強力なツールとなります。