How to make a safer C++ variant visitor, similar to switch statements?

核能气质少年 提交于 2019-12-03 06:38:31

问题


The pattern that a lot of people use with C++17 / boost variants looks very similar to switch statements. For example: (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);

The problem is when you put the wrong type in the visitor or change the variant signature, but forget to change the visitor. Instead of getting a compile error, you will have the wrong lambda called, usually the default one, or you might get an implicit conversion that you didn't plan. For example:

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 statements on enum classes are way more secure, because you can't write a case statement using a value that isn't part of the enum. Similarly, I think it would be very useful if a variant visitor was limited to a subset of the types held in the variant, plus a default handler. Is it possible to implement something like that?

EDIT: s/implicit cast/implicit conversion/

EDIT2: I would like to have a meaningful catch-all [](auto) handler. I know that removing it will cause compile errors if you don't handle every type in the variant, but that also removes functionality from the visitor pattern.


回答1:


If you want to only allow a subset of types, then you can use a static_assert at the beginning of the lambda, e.g.:

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

This will fails if you add or change a type in the variant, or add one, because T needs to be exactly one of the given types.

You can also play with your variant of std::visit, e.g. with a "default" visitor like:

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

If you add a type that is not one of int, long, double or std::string, then the visit_only_for call operator will be matching and you will have an ambiguous call (between this one and the default one).

This should also works without default because the visit_only_for call operator will be match, but since it is deleted, you'll get a compile-time error.




回答2:


You may add an extra layer to add those extra check, for example something like:

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

With usage:

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


来源:https://stackoverflow.com/questions/45707857/how-to-make-a-safer-c-variant-visitor-similar-to-switch-statements

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!