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.
std::visit from C++17 is a powerful utility that allows you to call a function over a currently active type in std::variant . In this post, I'll show you how to leverage all capabilities of this handy function: the basics, applying on multiple variants, and passing additional parameters to the matching function.
The class template std::variant represents a type-safe union. An instance of std::variant at any given time either holds a value of one of its alternative types, or in the case of error - no value (this state is hard to achieve, see valueless_by_exception).
boost::variant is defined in boost/variant. hpp . Because boost::variant is a template, at least one parameter must be specified. One or more template parameters specify the supported types. In Example 24.1, v can store values of type double , char , or std::string .
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With