Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 745
Michał Brzozowski Avatar asked Aug 16 '17 07:08

Michał Brzozowski


People also ask

What is STD visit?

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.

What is std :: variant in C++?

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

What is boost :: variant?

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 .


2 Answers

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.

like image 105
Holt Avatar answered Oct 08 '22 03:10

Holt


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); 
like image 45
Jarod42 Avatar answered Oct 08 '22 03:10

Jarod42