Подтвердить что ты не робот

Как сделать более безопасным посетителя варианта С++, как и инструкции switch?

Шаблон, который многие люди используют с вариантами С++ 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);

Операторы switch для классов enum более безопасны, потому что вы не можете написать оператор case, используя значение, которое не является частью перечисления. Точно так же я думаю, что было бы очень полезно, если вариант посетителя был ограничен подмножеством типов, содержащихся в варианте, плюс обработчик по умолчанию. Возможно ли реализовать что-то подобное?

EDIT: s/неявное литье/неявное преобразование /

EDIT2: Я бы хотел иметь полноценный обработчик [](auto). Я знаю, что удаление его приведет к ошибкам компиляции, если вы не обрабатываете каждый тип в варианте, но также удаляет функциональность из шаблона посетителя.

4b9b3361

Ответ 1

Если вы хотите разрешить только подмножество типов, вы можете использовать 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);

Если вы добавите тип, который не является одним из int, long, double или std::string, тогда оператор вызова visit_only_for будет соответствовать, и у вас будет неоднозначный вызов (между этим и по умолчанию).

Это также должно работать без по умолчанию, потому что оператор вызова visit_only_for будет соответствовать, но поскольку он удален, вы получите ошибку времени компиляции.

Ответ 2

Вы можете добавить дополнительный слой для добавления дополнительной проверки, например:

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