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