1+ /* *
2+ * EN: Real World Example of the Visitor Design Pattern (Modern C++17 Standard)
3+ *
4+ * Need: Consider a restaurant \c Menu represented as a heterogeneous \c Item
5+ * collection of different \c Food and \c Drink items, which must be
6+ * (homogeneously) serialised into RFC 8259 JSON for some external API usage.
7+ *
8+ * Solution: A modern C++17 standard \c Serialiser Visitor be easily implemented
9+ * using the built-in utilities found within the \c <variant> header, namely,
10+ * the type-safe union \c std::variant to represent different menu items and the
11+ * functor \c std::visit to apply a callable \c Serialiser visitor.
12+ *
13+ * This simpler ("KISS") and boilerplate-free implementation of the Visitor
14+ * Design Pattern surpasses the classical object-oriented programming Visitor
15+ * that often requires maintaining two separate, but cyclically interdependent,
16+ * class hierarchies:
17+ *
18+ * \c Item<-Food/Drink and \c Visitor<-FoodVisitor/DrinkVisitor
19+ *
20+ * and suffers from performance penalties associated with the virtual function
21+ * calls in the double dispatch.
22+ *
23+ * In this contemporary take on the Visitor Design Pattern here, the (SOLID)
24+ * Open-Closed Principle is more expressively fulfilled because the \c Food and
25+ * \c Drink classes do not need to be derived from some base \c Item class and
26+ * also do not need to be updated with \c AcceptVisitor methods. The absence of
27+ * any intrusive polymorphism provides greater flexibility; this means that new
28+ * types (i.e. new \c Item types such as \c Snack ) and new visitors (i.e. ) are
29+ * more straightforward to incorporate.
30+ *
31+ * For such a \e procedural Visitor Design Pattern, performances gains can be
32+ * expected if the \c std::variant uses value semantics rather than reference
33+ * semantics, and if the collection storage is continguous, that is, instead of
34+ * the memory-scattering pointer indirections of the traditional Visitor Design
35+ * Pattern applied to multiple types.
36+ */
37+
38+ #include < iostream>
39+ #include < string>
40+ #include < variant>
41+ #include < vector>
42+
43+ /* *
44+ * EN: Stable Low-Lying Data Structures for Food, Drink,...
45+ *
46+ * Respecting the Open-Closed Principle, there is no need to modify these
47+ * classes to accept the visitors that are to be introduced later. Observe that
48+ * these \c Item classes are not part of an inheritance hierarchy and so there
49+ * is flexibility to create more such \c Item classes.
50+ *
51+ * However, note that these classes require a complete definition here in lieu
52+ * of their upcoming role within the \c std::variant union, that is, a forward
53+ * declaration of these classes is not sufficient. The public API consists of an
54+ * explicit constructor and the necessary access methods that are required by
55+ * the \c Serialiser Visitor.
56+ */
57+ class Food {
58+ public:
59+ enum Label : unsigned { meat, fish, vegetarian, vegan };
60+
61+ public:
62+ explicit Food (std::string name, std::size_t calories, Label label)
63+ : name_{name}, calories_{calories}, label_{label} {}
64+
65+ auto name () const noexcept { return name_; }
66+ auto calories () const noexcept { return calories_; }
67+ auto label () const noexcept {
68+ switch (label_) {
69+ case Label::meat:
70+ return " meat" ;
71+ case Label::fish:
72+ return " fish" ;
73+ case Label::vegetarian:
74+ return " vegetarian" ;
75+ case Label::vegan:
76+ return " vegan" ;
77+ default :
78+ return " unknown" ;
79+ }
80+ }
81+
82+ private:
83+ std::string name_;
84+ std::size_t calories_;
85+ Label label_;
86+ };
87+
88+ class Drink {
89+ public:
90+ enum Label : unsigned { alcoholic, hot, cold };
91+
92+ public:
93+ explicit Drink (std::string name, std::size_t volume, Label label)
94+ : name_{name}, volume_{volume}, label_{label} {}
95+
96+ auto name () const noexcept { return name_; }
97+ auto volume () const noexcept { return volume_; }
98+ auto label () const noexcept {
99+ switch (label_) {
100+ case Label::alcoholic:
101+ return " alcholic" ;
102+ case Label::hot:
103+ return " hot" ;
104+ case Label::cold:
105+ return " cold" ;
106+ default :
107+ return " unknown" ;
108+ }
109+ }
110+
111+ private:
112+ std::string name_;
113+ std::size_t volume_;
114+ Label label_;
115+ };
116+
117+ /* ... */
118+
119+ /* *
120+ * EN: Variant Union of the Item and Menu as an Item Collection
121+ *
122+ * The \c Item and \c Menu aliases carve out an architectural boundary
123+ * separating the low-lying data structures (above) and the client-facing
124+ * visitor (below), the former being more established in the codebase and the
125+ * latter being perhaps newer and often more changeable. Also note the value
126+ * semantics, which means there is no need for manual dynamic memory allocation
127+ * or management (e.g. via smart pointers) and hence lower overall complexity
128+ * when it comes to implementing the Visitor Design Pattern.
129+ *
130+ * For best performance, it is recommended to use \c Item types of similar, if
131+ * not identical, sizes so that the memory layout can be optimised. (If there
132+ * are considerable differences in the class sizes, then it may be sensible to
133+ * use the Proxy Design Pattern to wrap around larger-sized classes or even the
134+ * Bridge Design Pattern/"pimpl" idiom.) The memory layout of the members within
135+ * each of the \c Item classes themselves may also be of importance to overall
136+ * performance (c.f. padding) in this implementation as the \c std::visit method
137+ * will be applied to each \c Item element by iterating over the \c Menu
138+ * container.
139+ */
140+ using Item = std::variant<Food, Drink /* ... */ >;
141+ using Menu = std::vector<Item>;
142+
143+ /* *
144+ * EN: Serialiser Visitor Functor
145+ *
146+ * This basic \c Serialiser class has non-canonical operator() overloads
147+ * which take the different \c Item types as input arguments, define lambdas to
148+ * perform a rudimentary conversion of the data to compressed/minified JSON
149+ * using the public API of the classes, and then print out the converted result
150+ * to some \c std::ostream by invoking the lambdas. Each \c Item has its own
151+ * unique overloaded operator() definition, which makes this class a prime
152+ * candidate for the Strategy Design Pattern e.g. different JSON specifications.
153+ */
154+ class Serialiser {
155+ public:
156+ explicit Serialiser (std::ostream &os = std::cout) : os_{os} {}
157+
158+ public:
159+ auto operator ()(Food const &food) const {
160+
161+ auto to_json = [&](auto f) {
162+ return R"( {"item":"food","name":")" + f.name () + R"( ","calories":")" +
163+ std::to_string (f.calories ()) + R"( kcal","label":")" + f.label () +
164+ R"( "})" ;
165+ };
166+
167+ os_ << to_json (food);
168+ }
169+ auto operator ()(Drink const &drink) const {
170+ auto to_json = [&](auto d) {
171+ return R"( {"item":"drink","name":")" + d.name () + R"( ","volume":")" +
172+ std::to_string (d.volume ()) + R"( ml","label":")" + d.label () +
173+ R"( "})" ;
174+ };
175+ os_ << to_json (drink);
176+ }
177+ /* ... */
178+
179+ private:
180+ std::ostream &os_{std::cout};
181+ };
182+
183+ /* ... */
184+
185+ /* *
186+ * EN: Applied Visitor for Menu (Item Collection) Serialisation
187+ *
188+ * The callable/invokable \c Serialiser Visitor can now be applied to each of
189+ * the \c Item elements in the \c Menu via the \c std::visit utility method, the
190+ * internal machinery of which could somewhat vary between different compilers
191+ * (e.g. GCC, Clang, MSVC, etc.) and their versions. Nevertheless, as a staple
192+ * part of the standard library from C++17 onwards, \c std::visit reliably and
193+ * conveniently automates the required boilerplate code and thereby reduces the
194+ * implementational friction that accompanies the traditional object-oriented
195+ * Visitor Design Pattern.
196+ *
197+ * Accordingly, it is now possible to perform a simple range-based for loop over
198+ * the \c Menu collection and apply visitor on each \c Item element in turn,
199+ * which has the best possible performance if the \c Item elements are stored
200+ * contiguously as values in memory.
201+ */
202+ void serialise (Menu const &menu, std::ostream &os = std::cout) {
203+ bool first{true };
204+ os << R"( {"menu":[)" ;
205+ for (auto const &item : menu) {
206+ if (!first)
207+ os << " ," ;
208+ else
209+ first = false ;
210+ std::visit (Serialiser{os}, item);
211+ }
212+ os << R"( ]})" ;
213+ }
214+
215+ /* ... */
216+
217+ /* *
218+ * EN: Client Code: Variant Visitor
219+ *
220+ * The declaration of the \c Menu collection is clean and hassle-free, and the
221+ * addition of the \c Item elements in form of \c Food and \c Drink class
222+ * instances is also drastically simplified by the value semantics. Finally, the
223+ * neat \c serialise method can be called with the \c Menu input argument to
224+ * demonstrate Modern C++17 Visitor Design Pattern in action.
225+ */
226+ int main () {
227+
228+ Menu menu;
229+ menu.reserve (8 );
230+
231+ menu.emplace_back (Food{" Borscht" , 160 , Food::Label::meat});
232+ menu.emplace_back (Food{" Samosa" , 250 , Food::Label::vegetarian});
233+ menu.emplace_back (Food{" Sushi" , 300 , Food::Label::fish});
234+ menu.emplace_back (Food{" Quinoa" , 350 , Food::Label::vegan});
235+ menu.emplace_back (Drink{" Vodka" , 25 , Drink::Label::alcoholic});
236+ menu.emplace_back (Drink{" Chai" , 120 , Drink::Label::hot});
237+ menu.emplace_back (Drink{" Sake" , 180 , Drink::Label::alcoholic});
238+ menu.emplace_back (Drink{" Kola" , 355 , Drink::Label::cold});
239+ /* ... */
240+
241+ serialise (menu);
242+ }
0 commit comments