1
+ /* *
2
+ * EN: Real World Example of the Singleton Design Pattern
3
+ *
4
+ * Need: Consider a (large) program that must implement its own internal logging
5
+ * functionality with a global logger object. Suppose that all log messages are
6
+ * required to be printed in order even if the logger is called across multiple
7
+ * concurrent threads or processes. Furthermore, the logger should have some
8
+ * sort of flag to specify and ignore messages below a certain level.
9
+ *
10
+ * Solution: A thread-safe Logger class can be implemented using the Scott
11
+ * Meyers' Singleton pattern. The Singleton pattern is the recommended solution
12
+ * if indeed there must be a single global instance of the Logger class.
13
+ * However, in modern practices, the addition of a new singleton to a codebase
14
+ * could be regarded as a design flaw with the singleton itself being a design
15
+ * anti-pattern. Nevertheless, the following presents a Logger Singleton as a
16
+ * commonly appearing use case of the pattern in the C++ literature.
17
+ */
18
+
19
+ #include < iostream>
20
+ #include < mutex>
21
+ #include < string>
22
+ #include < thread>
23
+
24
+ /* *
25
+ * EN: The Logger Singleton Class
26
+ *
27
+ * In this (zero handle objects) implementation of the Scott Meyers' Singleton,
28
+ * the constructor and destructor are private methods and the move/copy
29
+ * constructors and assignment operations are explicitly deleted. In essence,
30
+ * the program itself cannot directly create an instance of the Logger class,
31
+ * and instead the static instance() member function must be used to access it.
32
+ *
33
+ * The public API of this Logger has two main callbacks: (1) set the level of
34
+ * the Logger; and (2) log a message at given level. For convenience, these two
35
+ * client-facing methods wrap around the instance() member function in a
36
+ * thread-safe fashion. An integral counter member is also included to
37
+ * demonstrate that the message ordering is preserved.
38
+ *
39
+ * Note the final keyword specifier prevents inheritance, that is, it is not
40
+ * possible to extend this Logger Singleton and override its class methods.
41
+ */
42
+
43
+ class Logger final {
44
+ public:
45
+ /* *
46
+ * EN: Various levels for the log messages can be labelled here; the choice of
47
+ * the level member establishes a threshold below which log messages are
48
+ * ignored.
49
+ */
50
+ enum class Level : unsigned {
51
+ debug = 0 ,
52
+ info = 1 ,
53
+ warning = 2 ,
54
+ error = 3 ,
55
+ /* ... */
56
+ };
57
+
58
+ public:
59
+ /* *
60
+ * EN: The Public API of this Logger
61
+ *
62
+ * Note that both of these methods must be implemented in a thread-safe
63
+ * manner, hence the mutex as a static member.
64
+ */
65
+ static void level (Level);
66
+ static void log (std::string const &, Level level = Level::debug);
67
+
68
+ public:
69
+ /* *
70
+ * EN: Prevention of Copy and Move Construction
71
+ */
72
+ Logger (Logger const &) = delete ;
73
+ Logger (Logger &&) = delete ;
74
+
75
+ /* *
76
+ * EN: Prevention of Copy and Move Assigment Operations
77
+ */
78
+ Logger &operator =(Logger const &) = delete ;
79
+ Logger &operator =(Logger &&) = delete ;
80
+
81
+ /* *
82
+ * EN: Public Instantiator Method
83
+ *
84
+ * In a typical Singleton, this static member function would enable access to
85
+ * the Singleton. In this implementation of a Logger class, it is called
86
+ * inside of the bodies of the public API methods.
87
+ */
88
+ static Logger &instance ();
89
+
90
+ private:
91
+ /* *
92
+ * EN: Private Constructor and Destructor
93
+ */
94
+ Logger ();
95
+ ~Logger ();
96
+
97
+ private:
98
+ static std::mutex mutex_;
99
+ static std::ostream &os_;
100
+ static std::size_t count_;
101
+ static Level level_;
102
+ };
103
+
104
+ /* *
105
+ * EN: Static members of the Logger class need to be defined outside of the
106
+ * class itself.
107
+ */
108
+ std::mutex Logger::mutex_;
109
+ std::ostream &Logger::os_{std::cout};
110
+ std::size_t Logger::count_{0 };
111
+ Logger::Level Logger::level_{Logger::Level::debug};
112
+
113
+ /* *
114
+ * EN: Magic Static (c.f. Scott Meyers' Singleton)
115
+ *
116
+ * The instance() method creates a local static instance of the Logger class,
117
+ * which is guaranteed thread-safe initialisation without manual thread
118
+ * synchronisation. Note that this does not guarantee the thread safety of other
119
+ * members; the RAII (Resource Acquistion Is Initialisation) principle should be
120
+ * used to lock and unlock the mutex.
121
+ *
122
+ * Note that there will be a performance penalty each time this method is
123
+ * called as there will be a check to see if the instance has already been
124
+ * initialised.
125
+ */
126
+ Logger &Logger::instance () {
127
+ static Logger instance;
128
+ return instance;
129
+ }
130
+
131
+ /* *
132
+ * EN: Logger Level Modifier Method
133
+ *
134
+ * This thread-safe setter allows the client to alter the (global) level member
135
+ * of the Logger.
136
+ */
137
+
138
+ void Logger::level (Logger::Level level) {
139
+ std::lock_guard<std::mutex> lock (mutex_);
140
+ instance ().level_ = level;
141
+ }
142
+
143
+ /* *
144
+ * EN: Enummeration-to-String Helper Function
145
+ *
146
+ * This implementation is naive but nonetheless useful for distinguishing the
147
+ * different kinds of log messages.
148
+ */
149
+ std::string to_string (Logger::Level level) {
150
+ switch (level) {
151
+ case Logger::Level::debug:
152
+ return " [DEBUG]" ;
153
+ case Logger::Level::info:
154
+ return " [INFO]" ;
155
+ case Logger::Level::warning:
156
+ return " [WARNING]" ;
157
+ case Logger::Level::error:
158
+ return " [ERROR]" ;
159
+ /* ... */
160
+ default :
161
+ return " [LEVEL]" ;
162
+ }
163
+ };
164
+
165
+ /* *
166
+ * EN: Thread-Safe Log Method
167
+ *
168
+ * If the message level is at or above the threshold level of the Logger
169
+ * Singleton, then the counter is incremented and the message is printed.
170
+ * Otherwise, the message is ignored and the counter remains as is.
171
+ *
172
+ * Note again the usage of RAII for mutex locking/unlocking should this method
173
+ * be called in a thread.
174
+ */
175
+ void Logger::log (std::string const &message, Logger::Level level) {
176
+ std::lock_guard<std::mutex> lock (mutex_);
177
+ if (static_cast <int >(level) < static_cast <int >(instance ().level_ ))
178
+ return ;
179
+ instance ().os_ << ++instance ().count_ << ' \t ' << to_string (level) << " \n\t "
180
+ << message << ' \n ' ;
181
+ }
182
+
183
+ /* *
184
+ * EN: Constructor and Destructor
185
+ *
186
+ * The print statements indicate when these methods are called in the program.
187
+ */
188
+ Logger::Logger () { std::cout << " ****\t LOGGER\t START UP\t ****" << ' \n ' ; }
189
+ Logger::~Logger () { std::cout << " ****\t LOGGER\t SHUT DOWN\t ****" << std::endl; }
190
+
191
+ /* *
192
+ * EN: Client Code: Logger Singleton Usage
193
+ *
194
+ * The desired Log Level is set which also instantiates the Logger class; the
195
+ * log() methods can then be invoked e.g. via lambdas within different threads.
196
+ */
197
+ int main () {
198
+
199
+ std::cout << " //// Logger Singleton ////\n " ;
200
+
201
+ Logger::level (Logger::Level::info);
202
+
203
+ std::thread t1 (
204
+ [] { Logger::log (" This is just a simple development check." ); });
205
+ std::thread t2 (
206
+ [] { Logger::log (" Here are some extra details." , Logger::Level::info); });
207
+ std::thread t3 ([] {
208
+ Logger::log (" Be careful with this potential issue." ,
209
+ Logger::Level::warning);
210
+ });
211
+ std::thread t4 ([] {
212
+ Logger::log (" A major problem has caused a fatal stoppage." ,
213
+ Logger::Level::error);
214
+ });
215
+
216
+ t1.join ();
217
+ t2.join ();
218
+ t3.join ();
219
+ t4.join ();
220
+
221
+ return EXIT_SUCCESS;
222
+ }
0 commit comments