web-dev-qa-db-ja.com

switchステートメントと同様に、より安全なC ++バリアントビジターを作成する方法は?

多くの人がC++ 17/boostバリアントで使用するパターンは、switchステートメントに非常に似ています。例:( cppreference.comのスニペット

_std::variant<int, long, double, std::string> v = ...;

std::visit(overloaded {
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);
_

問題は、訪問者に間違ったタイプを入力したり、バリアントシグネチャを変更したが、訪問者の変更を忘れた場合です。コンパイルエラーを取得する代わりに、通常はデフォルトの間違ったラムダが呼び出されるか、計画していない暗黙的な変換が行われる可能性があります。例えば:

_v = 2.2;
std::visit(overloaded {
    [](auto arg) { std::cout << arg << ' '; },
    [](float arg) { std::cout << std::fixed << arg << ' '; } // oops, this won't be called
}, v);
_

Enumクラスのswitchステートメントは、enumの一部ではない値を使用してcaseステートメントを記述できないため、はるかに安全です。同様に、バリアントの訪問者が、バリアントに保持されている型のサブセットとデフォルトハンドラに制限されていれば、非常に役立つと思います。そのようなものを実装することは可能ですか?

編集:s /暗黙のキャスト/暗黙の変換/

EDIT2:意味のあるキャッチオール[](auto)ハンドラーが必要です。バリアント内のすべてのタイプを処理しない場合、それを削除するとコンパイルエラーが発生することはわかっていますが、ビジターパターンから機能も削除します。

30

型のサブセットのみを許可する場合は、ラムダの先頭でstatic_assertを使用できます。例:

template <typename T, typename... Args>
struct is_one_of: 
    std::disjunction<std::is_same<std::decay_t<T>, Args>...> {};

std::visit([](auto&& arg) {
    static_assert(is_one_of<decltype(arg), 
                            int, long, double, std::string>{}, "Non matching type.");
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int with value " << arg << '\n';
    else if constexpr (std::is_same_v<T, double>)
        std::cout << "double with value " << arg << '\n';
    else 
        std::cout << "default with value " << arg << '\n';
}, v);

Texactlyのいずれかのタイプである必要があるため、バリアントのタイプを追加または変更した場合、または追加した場合、これは失敗します。

std::visitのバリアントで遊ぶこともできます。次のような「デフォルト」の訪問者を使用します。

template <typename... Args>
struct visit_only_for {
    // delete templated call operator
    template <typename T>
    std::enable_if_t<!is_one_of<T, Args...>{}> operator()(T&&) const = delete;
};

// then
std::visit(overloaded {
    visit_only_for<int, long, double, std::string>{}, // here
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);

intlongdouble、またはstd::stringのいずれでもない型を追加すると、visit_only_for呼び出し演算子が一致し、あいまいな呼び出しが行われます(この呼び出しと既定の呼び出しの間)。

visit_only_for呼び出し演算子が一致するため、これもデフォルトで機能しますが、削除されるため、コンパイル時エラーが発生します。

24
Holt

次のような追加のチェックを追加するために、追加のレイヤーを追加できます。

template <typename Ret, typename ... Ts> struct IVisitorHelper;

template <typename Ret> struct IVisitorHelper<Ret> {};

template <typename Ret, typename T>
struct IVisitorHelper<Ret, T>
{
    virtual ~IVisitorHelper() = default;
    virtual Ret operator()(T) const = 0;
};

template <typename Ret, typename T, typename T2, typename ... Ts>
struct IVisitorHelper<Ret, T, T2, Ts...> : IVisitorHelper<Ret, T2, Ts...>
{
    using IVisitorHelper<Ret, T2, Ts...>::operator();
    virtual Ret operator()(T) const = 0;
};

template <typename Ret, typename V> struct IVarianVisitor;

template <typename Ret, typename ... Ts>
struct IVarianVisitor<Ret, std::variant<Ts...>> : IVisitorHelper<Ret, Ts...>
{
};

template <typename Ret, typename V>
Ret my_visit(const IVarianVisitor<Ret, std::decay_t<V>>& v, V&& var)
{
    return std::visit(v, var);
}

使用法で:

struct Visitor : IVarianVisitor<void, std::variant<double, std::string>>
{
    void operator() (double) const override { std::cout << "double\n"; }
    void operator() (std::string) const override { std::cout << "string\n"; }
};


std::variant<double, std::string> v = //...;
my_visit(Visitor{}, v);
1
Jarod42