📜 ⬆️ ⬇️

C ++ idioms. Static visitor

The Visitor pattern offers another way to separate the data processing algorithm from the data itself. In this article, I will briefly describe the idea behind the original pattern, its C ++ specific variation and give some simple examples of use.

Visitor


First, let's remember how the classic Visitor works . The motivation for this pattern is quite simple. Imagine that in the program we need to process a container (tree, graph) of polymorphic pointers and perform some set of operations for each object, and this set should be different for each specific type. It is also worth noting that the objects themselves should not know anything about the processing algorithms, except that the handler can “visit” them.
For example, file system objects: files, folders:

class abstract_file_t { public: virtual std::string name() const = 0; virtual void accept(visitor_t& v) = 0; virtual ~abstract_file_t(){} }; //////////////////////////////////////////////////////////////////////////// class regular_file_t : public abstract_file_t { public: std::string name() const; void accept(visitor_t& v); size_t size(); }; //////////////////////////////////////////////////////////////////////////// typedef std::vector<abstract_file_t*> file_vector_t; class directory_t : public abstract_file_t { public: void accept(visitor_t& v); std::string name() const; file_vector_t& files(); }; 

As you can see, the knowledge of file system objects about how they will work with them is only that they can be “ visited ” by an object with the base type visitor_t . In the accept function, we simply “admit the visitor”:

 void regular_file_t::accept(visitor_t& v) {v.visit(*this);} 

In the case of a directory, the code can be added to accept to “visit” all the files in it.
“Visitor” is arranged as follows:
')
 class visitor_t { public: virtual void visit(regular_file_t& f) = 0; virtual void visit(directory_t& dir) = 0; virtual ~visitor_t(){} }; class print_info_visitor_t : public visitor_t { public: void visit(regular_file_t& f); { std::cout << "visiting concrete file. file name: " << f.name() << " file size: " << f.size() << std::endl; } void visit(directory_t& dir) { std::cout << "visiting directory. directory name: " << dir.name() << ". contains " << dir.files().size() << “files” << std::endl; } }; 


Static visitor


The essence of Static visitor is also to separate the data from the algorithms for processing this data. The main difference lies in the fact that the dynamic polymorphism of the classic Visitor 'a is replaced with a static one (hence, the name of the idiom itself). With one implementation of this pattern, we meet almost every time we use STL algorithms. Indeed, STL predicates are a great example of a static visitor . To make this quite obvious, consider the following small example:

 class person_t { public: person_t(const std::string& name, size_t age) : name_(name), age_(age){} template<typename Visitor> void accept(Visitor& v) {v.visit(*this);} size_t age() const {return age_;} private: std::string name_; size_t age_; }; //////////////////////////////////////////////////////////////////////////////// struct person_visitor_t { person_visitor_t(size_t age_limit) : age_limit_(age_limit){} bool operator()(const person_t& p) {return visit(p);} bool visit(const person_t& p) {return p.age() < age_limit_;} size_t age_limit_; }; //////////////////////////////////////////////////////////////////////////////// int main() { std::vector<person_t> person_vec; person_vec.push_back(person_t("Person 1", 43)); person_vec.push_back(person_t("Person 2", 20)); auto it = std::find_if( person_vec.begin(), person_vec.end(), person_visitor_t(30)); if(it != person_vec.end()) std::cout << it->age() << std::endl; return 0; } 

Very similar to what we saw in the first chapter, isn't it?

Examples of using


Boost Graph Library

The idea of ​​a predicate can be developed. Why don't we allow the user to change the behavior of our algorithms at some key points with the help of the “visitor” provided by the user? Suppose we are writing a library for working with graphs, consisting of data structures for storing nodes and edges and algorithms for processing these structures ( Boost Graph Library ). For maximum flexibility, we can provide two options for each algorithm. One performs the default actions and the other allows the user to influence certain steps of the algorithm. Simplified it can be represented as:

 template<typename T> struct node_t { node_t(){} //   accept template<typename V> void on_init(V& v) {v.on_init(t_);} //   accept template<typename V> void on_print(V& v) {v.on_print(t_);} T t_; }; 

Algorithms. One default version and one using Visitor

 template<typename T, typename Graph> void generate_graph(Graph& g, size_t size); template<typename T, typename Graph, typename Visitor> void generate_graph(Graph& g, Visitor& v, size_t size) { for(size_t i = 0; i < size; ++i) { node_t<T> node; node.on_init(v); g.push_back(node); } } //////////////////////////////////////////////////////////////////////////////// template<typename Graph> void print_graph(Graph& g); template<typename Graph, typename Visitor> void print_graph(Graph& g, Visitor& v) { for(size_t i = 0; i < g.size(); ++i) { g[i].on_print(v); } } 

Now user code.

 struct person_t { std::string name; int age; }; //////////////////////////////////////////////////////////////////////////////// // visitor struct person_visitor_t { // visit() void on_init(person_t& p) { p.name = "unknown"; p.age = 0; } // visit() void on_print(const person_t& p) { std::cout << p.name << ", " << p.age << std::endl; } }; //////////////////////////////////////////////////////////////////////////////// int main() { person_visitor_t person_visitor; typedef std::vector<node_t<person_t> > person_vec_t; person_vec_t graph; generate_graph<person_t>(graph, person_visitor, 10); print_graph(graph, person_visitor); } 

Variant

Another very interesting example of the use of the idioms static visitor can be found in boost :: variant . Variant is a statically typed union . Data of any valid type is stored in the same byte array. And we “visit” as a matter of fact always this array stored inside the variant , but “look” at it every time from the point of view of different types. This can be implemented somehow (the code is simplified to the maximum and conveys only the main idea):

  template< typename T1 = default_param1, typename T2 = default_param2, typename T3 = default_param3 > class variant { ... public: //     accept() template<typename Visitor> void apply_visitor(const Visitor& v) { switch(type_tag_) //       { case 1: apply1(v, T1()); break; case 2: apply2(v, T2()); break; case 3: apply3(v, T3()); break; default: break; } } }; 

The apply functions may look like this.

  template<typename Visitor, typename U> void apply1( const Visitor& v, U u, typename std::enable_if< !std::is_same<U, default_param1>::value>::type* = 0) { // data_ -  . //   visit()  operator() v(*(T1*)(&data_[0])); } //     . template<typename Visitor, typename U> void apply1( const Visitor& v, U u, typename std::enable_if< std::is_same<U, default_param1>::value>::type* = 0) { } 


Here we use SFINAE to “turn on” the correct function for the current type and “turn it off” for the default class parameters. The user code is quite simple:

 struct visitor_t { void operator()(int i)const ; void operator()(double d)const; void operator()(const std::string& s)const; }; variant<int, double> test_v(34); test_v.apply_visitor(visitor_t()); 

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


All Articles