書いたもの
概要
プログラマの知人と通話をしていた時、あるコードパスで variant
の選択肢 (alternative) が限られる際にその限られた選択肢にのみ対応する visitor を渡したい需要があるという話をされ、そういったクラスを書いてくれ(意訳)と言われたので(一緒に)書きました。
#include <iostream>
#include <string>
#include <variant>
#include <yk/util/overloaded.hpp>
#include <yk/variant_view.hpp>
int main() {
std::variant<int, double, std::string> variant = 42;
// 本来書くべき visit
std::visit(yk::overloaded{
[](int x) { std::cout << "int : " << x << std::endl; },
[](double x) { std::cout << "double : " << x << std::endl; },
[](const std::string& s) { std::cout << "string : " << s << std::endl; },
},
variant);
// int か double かしか取らないと仮定する
auto variant_view = yk::make_variant_view<int, double>(variant);
// std::string を省略してもよい
yk::visit(yk::overloaded{
[](int x) { std::cout << "int : " << x << std::endl; },
[](double x) { std::cout << "double : " << x << std::endl; },
},
variant_view);
}
コピーコストがほぼ掛からない
「variant
の選択肢を狭めたい」と思って検索するとヒットするのは variant<A, B, C>
から variant<A, B>
への変換関数などでありコピーコストへの懸念が残りますが、 variant_view
では元の variant
を参照するだけとなります。
#include <cstddef>
#include <type_traits>
#include <utility>
#include <variant>
#include <yk/variant_view.hpp>
template <class T, class... Us>
struct is_in_types : std::disjunction<std::is_same<T, Us>...> {};
template <class... To, class... From, std::enable_if_t<std::conjunction_v<is_in_types<To, From...>...>, std::nullptr_t> = nullptr>
constexpr std::variant<To...> narrow_variant(const std::variant<From...>& variant) {
return std::visit(
[](auto&& arg) -> std::variant<To...> {
if constexpr (std::is_constructible_v<std::variant<To...>, decltype(arg)>) {
return std::variant<To...>{std::forward<decltype(arg)>(arg)};
} else {
throw std::logic_error("bad variant cast");
}
},
variant);
}
int main() {
std::variant<int, double, std::string> variant;
auto narrowed = narrow_variant<int, double>(variant); // 元の variant からコピー
auto view = yk::make_variant_view<int, double>(variant); // 元の variant を参照
}
ミスがあった際の実行時チェック
特定の型のみ考慮したい場合に大抵は何もしないオーバーロードを追加してコンパイルエラーを握りつぶすと思われますが、想定していない型が現れた際には何らかの実行時エラーがあるべきです。
variant_view
ではその際に std::bad_variant_access
例外を送出します。
std::variant<int, double, std::string> variant = "foo";
std::visit(yk::overloaded{
[](int x) { std::cout << "int : " << x << std::endl; },
[](double x) { std::cout << "double : " << x << std::endl; },
[](auto) { /* (エラー処理も含め)何もしない */ },
},
variant);
// => 何もエラーが出ずに終了
std::variant<int, double, std::string> variant = "foo";
auto variant_view = yk::make_variant_view<int, double>(variant);
yk::visit(yk::overloaded{
[](int x) { std::cout << "int : " << x << std::endl; },
[](double x) { std::cout << "double : " << x << std::endl; },
},
variant_view);
// => std::bad_variant_access 例外が投げられる
std::variant
との互換性
variant_view
は std::variant
の持つ機能をほぼ網羅しているので、std::variant
と同じように扱うことができます。
ただし std::get
や std::holds_alternative
へのオーバーロードの追加は規格的にダメなので独自の yk::get
や yk::holds_alternative
を用意しています。
boost::variant
への対応
件の知人は元々 boost::variant
(特にその再帰できる機能)を使っていたため、そちらにも対応しています。今のところ boost::variant2::variant
には対応していません。
機能概要
ソースコードは読めば分かるように書いているつもりですが、ドキュメント化という意味でも機能を書き下していきます。
variant_view
メインのクラスです。基本的に std::variant
のインターフェースに沿っていますが、 get
についてだけは変則的です(後述)。
コンストラクタ
デフォルトコンストラクタ
variant_view();
空の variant_view
を構築します。基本的に使うことはありません。
variant
からの構築
variant_view(Variant& variant);
variant_view(variant_type&& variant);
一番使うコンストラクタですが、基本的には後述の make_varianT_view
を使うことになると思います。
他の variant_view
からの変換
variant_view(const variant_view<V, Us...>& other);
選択肢を狭めるような変換しか許さないように制約を設けています。
base
const variant_type& base() const;
variant_type& base() const;
variant_view
が参照している variant
への参照を返します。
subview
variant_view<Variant, Us...> subview() const;
同じ variant
を参照しつつ、さらに選択肢を狭めます。
invalid
bool invalid() const
デフォルト構築した variant_view
はどの variant
も参照しないため無効な状態であり、そのような状態であることを確かめるためのメンバ関数です。
visit
template <class Visitor>
decltype(auto) visit(Visitor&& vis) const;
template <class Res, class Visitor>
Res visit(Visitor&& vis) const;
メンバ関数版 visit
です。
index
std::size_t index() const;
std::variant
の index
および boost::variant
の which
をそのまま呼びます。
狭めた選択肢に対する index でないことに注意してください。
比較演算子
宣言は省略しますが、元の variant
の比較演算子をそのまま呼び出します。
sizeof...(Ts) == 1
の特殊化について
decltype(auto) operator*() const;
auto operator->() const;
operator bool() const;
選択肢が一つまで狭まった時にわざわざ get
や visit
を呼ぶのは手間なので、ポインタライクな簡易的なアクセスを追加で提供します。
get
template <class T, class VariantView>
decltype(auto) get(VariantView&& view);
template <std::size_t I, class VariantView>
decltype(auto) get(VariantView&& view);
template <class T, class VariantView>
decltype(auto) get(const VariantView* view);
template <std::size_t I, class VariantView>
decltype(auto) get(const VariantView* view);
std::get
と同様ですが、 std::get_if
相当の機能も含んでいます。これは boost::get
のインターフェースと合わせてあります。variant_view
だけでなく std::variant
や boost::variant
にも使えます。
visit
template <class Visitor, class Variant>
decltype(auto) visit(Visitor&& vis, Variant&& variant);
template <class Res, class Visitor, class Variant>
Res visit(Visitor&& vis, Variant&& variant);
非メンバ関数版 visit
です。 variant_view
だけでなく std::variant
や boost::variant
にも使えます。
make_variant_view
variant_view
を作るヘルパ関数です。CTAD では限界があるので、使いやすさのために提供しています。
std::variant<int, double, std::string> variant = 42;
// int, double に狭める
auto view_1 = yk::make_variant_view<int, double>(variant);
// int のみに狭める
auto view_2 = yk::make_variant_view<int>(variant);
// 狭めずに variant_view を作る
auto view_3 = yk::make_variant_view(variant);
holds_alternative
template <class T, class Variant>
bool holds_alternative(const Variant& variant);
variant
や variant_view
が型 T
を保持しているかどうかを返します。
空の variant_view
に対しても false
を返します。
最後に
実用できそうなら教えて下さい。バグ取りも多分します。