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