概要
variantを使おうとした際、値の取得法にいくつか選択肢があることに気が付く。(std::holds_alternative + std::get, std::get_if, std::visit)
それらの使い方、使い分けについてまとめる
取得方法
各関数の詳細は https://ja.cppreference.com/w/cpp/utility/variant とか見てください
std::holds_alternative + std::get
「今variantに入ってる型はTか?」を判定する関数 std::holds_alternative
と
「variantから型Tを取り出す」関数 std::get
による取得
std::variant<int,std::string> v="cfn";
if(std::holds_alternative<std::string>(v)){
std::cout<< std::get<std::string>(v);
}
std::get
には 型を指定する代わりにindexを指定することで値を取得する方法もある
しかしこれはマジックナンバーを生み出すことに繋がるため、あまりオススメしない
std::variant<int,std::string> v="abcd";
if(v.index() == 1){ // variant.index()は入ってる要素のindexを調べる関数
std::cout<< std::get<1>(v); // <== 1ってなんだよ
}
std::get_if
variantが指定した型Tであるなら値へのポインタを、そうでないならnullptr
を返す関数
要するにstd::holds_alternative
std::get
を一度にやる関数である
std::variant<int,std::string> v="abcd";
if(auto p = std::get_if<std::string>(&v)){ // ポインタで渡す
std::cout<< *p;
}
std::get
と同じように indexを指定することで値を取得する方法もあるが、
同じ理由で indexによる指定はオススメしない
std::visit
「渡された関数にvariantが持ってる値を適用する」関数である
すべての型に対し同じ処理を行いたいとき、特に便利
std::variant<int,double> v=3.14;
// 例1 variantから値を返す
std::string s = std::visit([&](auto x){ return std::to_string(x);}, v); // variantの値をstringに変換して 返す
std::cout<< s;
// 例2 variantから値を取り出して副作用を起こしたりする
std::string buf;
std::visit([&](auto x){ // xは intかdoubleになる
buf+=std::to_string(x);
std::cout << x;
}, v);
型によって処理を変えたい場合、 if constexpr
や 関数のオーバーロードなどを利用すればよい
//if constexprの場合
std::variant<int,std::string> v="abcd";
std::visit([](auto&& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "int: " << x;
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "std::string: " << x;
}, v);
// 前準備。 渡された(ラムダ式等の)関数オブジェクトをオーバーロード状にするやつ
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
// コード
std::variant<int,std::string> v="abcd";
std::visit(overloaded {
[](int n) { std::cout << "int: " << e; },
[](std::string e) { std::cout << "string: " << e; }
// Event3にマッチする処理がないのでコンパイルエラーになる
}, v);
visitはvariantを複数うけとり処理することもできる
// a,b 同じ型なら上の関数を、それ以外なら下を呼び出す ちょっとパターンマッチ的な処理
// C++20では `[]<class T>(T a,T b){/*実装*/}` のようにラムダ式でも書けるようになっている(ハズ)
template<class T>
std::optional<bool> hoge(T a,T b){return a == b;}
template<class T, class U>
std::optional<bool> hoge(T,U){ return {};}
int main(){
std::variant<int,std::string> v1="abcd";
std::variant<int,std::string> v2="1234";
std::visit([](auto a,auto b){return hoge(a,b);}, v1,v2);
}
使い分けのヒント
共通処理なら
すべての型に共通処理を行いたいなら
前述のとおり std::visit
がおすすめ
std::variant<hoge1,hoge2,hoge3> v;
std::visit([&](auto x){ do_hoge(x);}, v); // すべてに同じ処理をする
ある特定の型のみに対して処理を行いたい
ある型数個のみに処理行いたいなら std::get_if
を使うのがおすすめ
std::variant<hoge1,hoge2,hoge3> v;
// hoge2ならなんかやる
if(auto p = std::get_if<hoge2>(&v)){
do_hoge(*p);
}
// 他はどうでもいい
variantの拡張に強くしたいなら
用途にもよるがvariant の要素というのは増えることもある
// イベントのキュー
// 今はEvent1,2だけだが 3を増やしたい。そしていつか4を増やすかもしれない
std::vector<std::variant<Event1, Event2/*, Event3*/>> event_queue;
この時、下のようなコードを書いているとEvent3に関する処理が抜けてしまう。
こういうのはコンパイル時に検出したい
// 例1
std::visit([](auto&& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, Event1>)
std::cout << "event1: " << x;
else if constexpr (std::is_same_v<T, Event2>)
std::cout << "event2: " << x;
// event3の処理がない。だが警告も出ない
}, v);
// 例2 get_ifの場合
if(auto p = std::get_if<Event1>(&v)){
std::cout<< *p;
}
else if(auto p = std::get_if<Event2>(&v)){
std::cout<< *p;
}
// event3の処理がない。警告も出ない
じゃぁどうすればいいかというと、以下の方法が考えられる
- visitで受けて
if constexpr
の分岐の最後にelse static_assert(false_v<T>);
書く - オーバーロードで知らない型を型レベルで拒否するようにすればよい
if constexpr + static_assert
前述のとおり 分岐の最後に else static_assert(false_v<T>);
とでも書いておけばよい
false_vについては この記事などを参照
https://qiita.com/saka1_p/items/e8c4dfdbfa88449190c5
// 前準備
template <typename T>
constexpr bool false_v = false;
// コード
std::visit([](auto&& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, Event1>)
std::cout << "event1: " << x;
else if constexpr (std::is_same_v<T, Event2>)
std::cout << "event2: " << x;
else
static_assert(false_v<T>); // ここに処理が来る(=Event1,2以外の型がくる)とコンパイルに失敗する
}, v);
オーバーロード
オーバーロードを用いれば型レベルで拒否できる
// 前準備
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
// コード
std::visit(overloaded {
[](Event1 e) { std::cout << "event1: " << e; },
[](Event2 e) { std::cout << "event2: " << e; }
// Event3にマッチする処理がないのでコンパイルエラーになる
}, v);
std::visit+ (if constexpr OR オーバーロード) どちらを使えばいいのか
- パターンマッチ的なことをやる場合オーバーロードのほうが簡単かもしれない
- 複雑なことをやるならif constexprのほうが面倒が少ない
- 簡単な問題なら大差ないので別にどっちでもいい
オーバーロードで複雑なことをしようと思うと SFINAEを使ったり 優先順位に気を使い始めたりすることになり、あまり簡単ではない
しかし一方、パターンマッチ的なことやSFINAEを使った方が便利な場合はオーバーロードで解決した方がうまくいくこともある
ラムダ式でSFINAE
https://qiita.com/Fuyutsubaki/items/e78a133545d52b84fa97
メモ
holds_alternative の使い道
holds_alternative が必要な場面なら全部get_ifでいいんじゃないか?と思うかもしれないが、例えば以下のよう場合必要になるかもしれない
- "型Tではない時" を調べたいとき
- 正規表現でいう
[^a]
的なことをやりたいときなど - 値をgetする意味がない時
- emptyな型(std::monostateとか)や値に興味がない場合など
構文解析っぽい処理をやってると必要になるかもしれない
『variantの拡張に強くしたいなら』別解法
でたらめな型を返す
static_assertの代わり適当な型を返すコードを書いておく
// 例1
if constexpr (std::is_same_v<T, Event1>)
return 42;
else
return no_reach_kill{}; // 返す値が int と異なるのでコンパイルエラーを引き起こす
// 例2
if constexpr (std::is_same_v<T, Event1>)
std::cout << "event1: " << x; // ここで返る型はvoidになる(返る型がない)ので、何かまともな値が返ると
else
return []{}; // でたらめな型ならラムダ式でも何でもいいよ
このようにすることで 他の型が来た時に返す型と違う型をstd::visitから返そうとして、結果コンパイルエラーになる
利点
- false_v、というかtwo phase lookupに関する知識がいらない
- 必要タイプ数が短いかもしれない
欠点
- 出力されるエラーメッセージがgcc・clang共に大分わかりにくい (https://wandbox.org/permlink/WtuVvwAvndJf6zzg)
- 意図がわかりづらい
variantの要素数でstatic_assertをする
下記の通り。要素数が変わったとき対応できないことを表明する
static_assert(std::variant_size_v<decltype(v)> == 2);
コメントで何をどう修正すればいいかを書いておけばこれでもいい気はしてる