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.
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.
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