如何制作更安全的C ++变体访问者,类似于switch语句?

时间:2017-08-16 07:45:26

标签: c++ switch-statement c++17 variant visitor

许多人使用C ++ 17 / boost变体的模式看起来与switch语句非常相似。例如:(snippet from 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);

问题是当您在访问者中输入错误的类型或更改变体签名时,但忘记更改访问者。您将获得错误的lambda,通常是默认的lambda,而不是获得编译错误,或者您可能会得到一个您没有计划的隐式转换。例如:

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

关于枚举类的switch语句更安全,因为你不能使用不是enum的一部分的值来编写case语句。同样,我认为如果变体访问者仅限于变体中包含的类型的子集,加上默认处理程序,那将非常有用。是否可以实现类似的东西?

编辑:s /隐式演员/隐式转换/

EDIT2:我想拥有一个有意义的catch-all [](auto)处理程序。我知道如果您不处理变体中的每个类型,删除它将导致编译错误,但这也会从访问者模式中删除功能。

3 个答案:

答案 0 :(得分:24)

如果您只想允许类型的子集,那么您可以在lambda的开头使用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);

如果您在变体中添加或更改类型,或者添加一个类型,则会失败,因为T需要完全其中一个给定类型。

您还可以使用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);

如果您添加的类型不是intlongdoublestd::string,则visit_only_for来电运营商将匹配并且你会有一个模糊的呼叫(在这个和默认呼叫之间)。

这也应该没有默认值,因为visit_only_for调用运算符将​​匹配,但由于它被删除,您将收到编译时错误。

答案 1 :(得分:1)

您可以添加额外的图层来添加这些额外的检查,例如:

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

答案 2 :(得分:0)

有点基于 Holtvisit_only_for 示例,我目前正在尝试类似的方法,以便在我的 std::visit 调用中插入一个“标签”,以防止忘记显式处理程序/运营商:

//! struct visit_all_types_explicitly
//!
//! If this callable is used in the overload set for std::visit
//! its templated call operator will be bound to any type
//! that is not explicitly handled by a better match.
//! Since the instantiation of operator()<T> will trigger
//! a static_assert below, using this in std::visit forces
//! the user to handle all type cases.
//! Specifically, since the templated call operator is a
//! better match than call operators found via implicit argument
//! conversion, one is forced to implement all types even if
//! they are implicitly convertible without warning.
struct visit_all_types_explicitly {
    template<class> static inline constexpr bool always_false_v = false;

    // Note: Uses (T const&) instead of (T&&) because the const-ref version
    //       is a better "match" than the universal-ref version, thereby
    //       preventing the use of this in a context where another
    //       templated call operator is supplied.
    template<typename T>
    void operator()(T const& arg) const {
        static_assert(always_false_v<T>, "There are unbound type cases! [visit_all_types_explicitly]");
    }
};

using MyVariant = std::variant<int, double>;

void test_visit() {
    const MyVariant val1 = 42;

    // This compiles:
    std::visit(
        overloaded{
            kse::visit_all_types_explicitly(),
            [](double arg) {},
            [](int arg) {},
        },
        val1
        );

    // does not compile because missing int-operator causes
    // visit_all_types_explicitly::operator()<int> to be instantiated
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
        },
        val1
        );

    // does also not compile: (with static assert from visit_all_types_explicitly)
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
            [](auto&& arg) {}
        },
        val1
    );

    // does also not compile: (with std::visit not being able to match the overloads)
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
            [](auto const& arg) {}
        },
        val1
    );
}

就目前而言,这似乎做我想做的,以及 OP 要求的:

<块引用>

您不会得到编译错误,而是会调用错误的 lambda,通常是默认的,或者您可能会得到一个您没有计划的隐式转换。

您不能故意将其与“默认”/自动处理程序结合使用。