エンジニアとしての市場価値を測りませんか?PR

企業からあなたに合ったオリジナルのスカウトを受け取って、市場価値を測りましょう

6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

知人に variant の選択肢(alternative)を狭めるクラスを書かされた話

Posted at

書いたもの

概要

プログラマの知人と通話をしていた時、あるコードパスで variant の選択肢 (alternative) が限られる際にその限られた選択肢にのみ対応する visitor を渡したい需要があるという話をされ、そういったクラスを書いてくれ(意訳)と言われたので(一緒に)書きました。

example.cpp
#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_viewstd::variant の持つ機能をほぼ網羅しているので、std::variant と同じように扱うことができます。
ただし std::getstd::holds_alternative へのオーバーロードの追加は規格的にダメなので独自の yk::getyk::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::variantindex および boost::variantwhichそのまま呼びます。
狭めた選択肢に対する index でないことに注意してください。

比較演算子

宣言は省略しますが、元の variant の比較演算子をそのまま呼び出します。

sizeof...(Ts) == 1 の特殊化について

decltype(auto) operator*() const;
auto operator->() const;
operator bool() const;

選択肢が一つまで狭まった時にわざわざ getvisit を呼ぶのは手間なので、ポインタライクな簡易的なアクセスを追加で提供します。

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::variantboost::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::variantboost::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);

variantvariant_view が型 T を保持しているかどうかを返します。
空の variant_view に対しても false を返します。

最後に

実用できそうなら教えて下さい。バグ取りも多分します。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?