Как сделать более безопасным посетителя варианта С++, как и инструкции 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)
. Я знаю, что удаление его приведет к ошибкам компиляции, если вы не обрабатываете каждый тип в варианте, но также удаляет функциональность из шаблона посетителя.
Ответы
Ответ 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);