🧘

std::variant変数からスマートに値を取得する

に公開

久しぶりにstd::variantを使う機会がありました。std::variantunionの型安全な C++17版のような、複数の型のうちのいずれか一つを保持できるコンテナです。
ユースケースとしては、完全に違う型の値を保存しないといけないですが、事前に型のリストが分かっている場合になります。
事前に型のリストが分からない場合は、std::anyの方が適切です。

例としては、intstd::stringの型の値を保存したいとしましょう。その場合は、以下のようにstd::variantになります。

std::variant<int, std::string> key;

ここまでは分かりやすいのすが、keyに今どの型が入っているかをチェックするにはどうすればいいかを調べると、std::variantのお勧めの使い方は大体以下のようになります。

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

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // こちらは C++20 からは不要になりました

int main()
{
    std::variant<int, std::string> key("test");

    std::visit(overloaded{
        [](int arg) { std::cout << arg << std::endl; },
        [](const std::string& arg) { std::cout << arg << std::endl; }
    }, key);
}

std::visitというユーティリティ関数に、各型の場合の関数を指定することで、型によって何をするのかが分かりやすい一覧が出来上がります。
この方法の一番いいところは、std::visitにすべての型の場合の関数を指定しないとエラーになることです。
例えば、std::variantに新しい型を追加した場合、アクセスする時にいつもstd::visitを使っていたら、各箇所に新しい型の場合の関数を追加しないとエラーになるため、対応が漏れることはないです。

でもstd::visitを使うために毎回overloadedのような訳の分からないテンプレートを定義しないといけない、というのが個人的に苦手です。
これをどこかのヘッダーで定義しておくとか、何らかの手段でもっと使いやすくはできたはずです。C++20 で1つだけになりましたが、それでも少し不便です。

でも、std::visit以外の使い方もあります。以下のように、std::holds_alternative<T>で現在の型をチェックして、std::get<T>で値を取得します。

if (std::holds_alternative<int>(key))
{
    const auto arg = std::get<int>(key);
    std::cout << arg << std::endl;
}
else if (std::holds_alternative<std::string>(key)) // ここは else でもいいです
{
    const auto arg = std::get<std::string>(key);
    std::cout << arg << std::endl;
}

でもなんか長いですね。これだと、std::visitの方が使いたくなります。ここで、今回発見したstd::get_ifの登場です!

if (const auto arg = std::get_if<int>(&key))
{
    std::cout << arg << std::endl;
}
else if (const auto arg = std::get_if<std::string>(&key))
{
    std::cout << arg << std::endl;
}

上のチェックと値の取得を一気にできる関数!これで勝つる!
選択肢の型が多い場合は、std::visitの方が見やすくてお勧めですが、2、3択の場合は、こちらの方が使いやすいと思います。
あ、でもstd::get_ifはポインタ型を渡さないといけないので、注意です!
後、std::variantに型を追加するかもしれない場合は、std::visitの方がすべての型の場合の関数を指定しないとエラーを返すため、安全でお勧めです。


|cpp記事一覧へのリンク|

Discussion