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

后端 未结 2 524
孤城傲影
孤城傲影 2021-02-02 08:41

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::varia         


        
相关标签:
2条回答
  • 2021-02-02 09:00

    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);
    
    0 讨论(0)
  • 2021-02-02 09:07

    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.

    0 讨论(0)
提交回复
热议问题