std::variant変数からスマートに値を取得する
久しぶりにstd::variant
を使う機会がありました。std::variant
はunion
の型安全な C++17版のような、複数の型のうちのいずれか一つを保持できるコンテナです。
ユースケースとしては、完全に違う型の値を保存しないといけないですが、事前に型のリストが分かっている場合になります。
事前に型のリストが分からない場合は、std::any
の方が適切です。
例としては、int
とstd::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
の方がすべての型の場合の関数を指定しないとエラーを返すため、安全でお勧めです。
Discussion