📜 ⬆️ ⬇️

Simple inline visitor for boost :: variant

Hi, Habr.

One fine Friday evening I wrote error handling in one of my hobby projects ... So, this is an introduction for another article.
In general, one fine Friday evening I needed to go through the boost::variant and do something with the data lying there. The standard task for boost::variant , and the canonical (but very verbose) way to solve it, is to describe the structure inherited from boost::static_visitor with overloaded operator() and pass it to boost::apply_visitor . And this wonderful evening for some reason I became very lazy to write all this bunch of code, and I wanted to have some simpler and more concise way of describing the visitors. What came of it, you can read under the cut.

So, the canonical way looks something like this:
 using Variant_t = boost::variant<int, char, std::string, QString, double, float>; template<typename ValType> struct EqualsToValTypeVisitor : boost::static_visitor<bool> { const ValType Value_; EqualsToValTypeVisitor (ValType val) : Value_ { val } { } bool operator() (const std::string& s) const { return Value_ == std::stoi (s); } bool operator() (const QString& s) const { return Value_ == s.toInt (); } template<typename T> bool operator() (T val) const { return Value_ == val; } }; void DoFoo (const Variant_t& var) { const int val = 42; if (boost::apply_visitor (EqualsToValTypeVisitor<int> { val }, var)) // ... } 

And we also took advantage of the fact that the four cases for int , char , float and double can be described by one template operator, otherwise the operators would be three more, the code would be even more bloated, and it would look still more terrible.

In addition, when the function-handlers of specific types are short, it is somehow a shame to get a separate structure for them, pull them away from the function in which they are used, and so on. You also have to write a constructor, if you need to transfer some data from the point of application of the visitor to the visitor itself, you have to create fields for this data, you have to keep track of copying, links and other things. It all starts not very pleasant to smell.
')
A natural question arises: is it possible to somehow define the visitors directly at the place of use, and with a minimum of the syntactic overhead? Well, to straight
 void DoFoo (const Variant_t& var) { const int val = 42; const bool isEqual = Visit (var, [&val] (const std::string& s) { return val == std::stoi (s); }, [&val] (const QString& s) { return val == s.toInt (); }, [&val] (auto other) { return other == val; }); } 


It turns out you can.

Since the solution turns out to be surprisingly simple and elegant in its own way, and writing it at once is not interesting (the article is painfully short), I will also describe a little how I came to this decision, so you can skip the next two or three paragraphs.

My first attempt, in the implementation details of which I will not go into (but which can be poked here ), was stuffing all lambdas into std::tuple and sequentially iterating them into the template operator() its own class, storing them until some time some function with an argument passed to operator() .

The obvious disadvantage of this solution is the fatally incorrect processing of types that are brought together, and the dependence on the order of lambda transfer to the visitor creation function. So, consider the above-mentioned Variant_t , which has among others int and char . If it was created with the char type, and the lambda receiving the int was first transferred to the visitor creation function, then it will be called first (and successfully!), And the case for char will not come. Moreover, this problem is really fatal: for the same int and char it is impossible (at least, without significant distortions) to determine the order of lambdas so that for both int and char passed to the right place, without any type conversions.

However, now it is worth remembering what lambda is and how it is developed by the compiler. And it unfolds into an anonymous structure with the overridden operator() . And if we have a structure, then it can be inherited from it, and its operator() will automatically be in the corresponding scope. And if you inherit from all structures at once, then all their operator() 's will go where necessary, and the compiler will automatically select the operator to call with each specific type, even if the types are given into each other (as in the above-mentioned case int and char ) .

And then - the case of technology and variadic templates:
 namespace detail { template<typename... Args> struct Visitor : Args... // ,     variadic pack { Visitor (Args&&... args) : Args { std::forward<Args> (args) }... //    { } }; } 


Let us try to write a function that takes boost::variant and a set of lambdas and visits this very variant :
 template<typename Variant, typename... Args> auto Visit (const Variant& v, Args&&... args) { return boost::apply_visitor (detail::Visitor<Args...> { std::forward<Args> (args)... }, v); } 


Oops, got a compilation error. apply_visitor expects to get a successor to boost::static_visitor , at least in my version of Boost 1.57 (they say that support for automatic output of the returned type was added later in C ++ 14-mode).

How to get the return type? You can try, for example, to take the first lambda from the list and call it with an object constructed by default, something like
 template<typename Variant, typename Head, typename... TailArgs> auto Visit (const Variant& v, Head&& head, TailArgs&&... args) { using R_t = decltype (head ({})); //return boost::apply_visitor (detail::Visitor<Head, TailArgs...> { std::forward<Head> (head), std::forward<TailArgs> (args)... }, v); } 

In this case, we naturally assume that all lambdas return the same type (or, more precisely, all returned types are convertible into each other).

The problem with this solution is that this very object may not have a default constructor. std::declval will not help us here either, because the type taken by the first lambda is not known in advance, and trying to call it with all types in a row from the type list of the variant is too crutchy and verbose.

Instead, we will do the opposite. We will take the first type from the variant type list and call our already constructed Visitor with it. This is guaranteed to work, because the visitor must be able to handle any of the types in the variant . So:
 template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) -> { using R_t = decltype (detail::Visitor<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())); //return boost::apply_visitor (detail::Visitor<Args...> { std::forward<Args> (args)... }, v); } 


However, Visitor itself must inherit from boost::static_visitor<R_t> , and R_t at this point. Well, it's quite easy to solve by breaking the Visitor into two classes, one of which deals with inheriting from lambdas and aggregating their operator() 's, and the other implements boost::static_visitor .

Total we get
 namespace detail { template<typename... Args> struct VisitorBase : Args... { VisitorBase (Args&&... args) : Args { std::forward<Args> (args) }... { } }; template<typename R, typename... Args> struct Visitor : boost::static_visitor<R>, VisitorBase<Args...> { using VisitorBase<Args...>::VisitorBase; }; } template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) { using R_t = decltype (detail::VisitorBase<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())); return boost::apply_visitor (detail::Visitor<R_t, Args...> { std::forward<Args> (args)... }, v); } 

For compatibility with C ++ 11, you can add trailing return type
 template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) -> decltype (detail::VisitorBase<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())) 


A nice bonus is the ability to work with noncopyable lambdas (exciting unique Cptr in C ++ 14 style, for example):
 #define NC nc = std::unique_ptr<int> {} Variant_t v { 'a' }; const auto& asQString = Visit (v, [NC] (const std::string& s) { return QString::fromStdString (s); }, [NC] (const QString& s) { return s; }, [NC] (auto val) { return QString::fromNumber (val); }); 


The disadvantage is the impossibility of a more subtle pattern-matching in the style
 template<typename T> void operator() (const std::vector<T>& vec) { //... } 

Unfortunately, [] (const std::vector& vec) {} . C++17. [] (const std::vector& vec) {} . C++17.

Source: https://habr.com/ru/post/270689/


All Articles