Within this article I want to show some base implementation techniques and demonstrate them in the context of a logging class. The logging class should able to write logging information into a file. The file access should be thread safe. Therefore, we should manage a file and a locking resource. With respect to the resource management needs and the multithreaded use of the logger, we will use the following base implementation techniques: RAII, error handling, move ctor and move assignment, copy ctor and copy assignment, multithreading support and singleton pattern.
Resource management with RAII
To manage the file and the lock resources, we will use the RAII concept. The resources will be managed by objects. We will start by implementing the logger class and create a parametrized ctor. According to the RAII concept, this ctor will initialize the file stream. The second resource, the locking object, is needed in the output function. This output function which will add a logging message can be called by different threads. Therefore, we use a locking mechanism. The std class “lock_guard” implements the RAII concept and fulfills out needs.
The following source code shows a possible implementation including error handling which is explained next. The implementation so far will create compiler errors. Within the next chapter we will look at the different constructor’s and expand the example so it becomes executable.
// ----- header file ----- #pragma once #include #include #include namespace Logging { class MyLogger { public: // parametrized ctor MyLogger(std::string fileName); // dtor ~MyLogger() { mStream.close(); } // write to log file void WriteLine(std::string content); private: std::ofstream mStream; std::mutex mMutex; }; } // ----- source file ----- #include "stdafx.h" #include #include #include #include "MyLogger.h" namespace Logging { MyLogger::MyLogger(std::string fileName) { mStream.open(fileName); if (mStream.fail()) { throw std::iostream::failure("Cannot open file: " + fileName); } } void MyLogger::WriteLine(std::string content) { std::lock_guard lock(mMutex); std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); char timestamp[26]; ctime_s(timestamp, sizeof timestamp, &now); std::string timestampWithoutEndl(timestamp); timestampWithoutEndl = timestampWithoutEndl.substr(0, 24); mStream << timestampWithoutEndl << ": " << content << std::endl; } }
Furthermore, we have to think about error handling. Within the parametrized ctor we want to open the file stream. This may fail. According to the documentation of “ofstream” the “open” function will normally not throw an exception and we have to check success by using the “fail” method. But it is possible to configure the streams in a way they will throw exceptions. As this is a global configuration for all streams we may not be sure whether some other component enables this behavior. In case logging is a mandatory feature and we ensure a correct file-handling including access rights on directories, we may decide that an error on opening the file should be an exceptional case. So, we should throw an error in this case. As the “open” function may already throw an exception of type “ios_base::failure” we will check for the non-exception behavior and throw an own exception of same type in this case.
Move vs. copy
With respect to the resource management we have to think about an important question: what do we expect if a copy of a class instance is created? The instance manages a file stream. Should the copy access the same file stream too? Of course not. This will bypass the RAII concept and leads to several issues. Therefore, we will not allow to create a copy of the instance. If someone wants to write to two different files, two different instances have to be created. But we may allow to pass the class instance into another scope, e.g. within a function call. As we don’t want to copy the class instance we have to use another technique: move ctor and move assignment. In case we move the class instance we can steal the resources from the source class and transfer it to the target class. As the source class instance will be deleted anyway this steal of resources is allowed.
The following source code shows an according implementation. The copy ctor and copy assignment operator are disabled and the move ctor and move assignment is implemented.
#pragma once #include #include #include namespace Logging { class MyLogger { public: // parametrized ctor MyLogger(std::string fileName); // disable copy ctor and copy assignment MyLogger(const MyLogger&) = delete; MyLogger& operator= (const MyLogger&) = delete; // move ctor and move assignment MyLogger(MyLogger&& other) { mStream.close(); mStream = move(other.mStream); } MyLogger& operator=(MyLogger&& other) { mStream.close(); mStream = move(other.mStream); return *this; } // dtor ~MyLogger() { mStream.close(); } // write to log file void WriteLine(std::string content); private: std::ofstream mStream; std::mutex mMutex; }; }
Client application
Now we can compile the source code and use it within a client application. The following console application contains some examples how to use the logger.
int _tmain(int argc, _TCHAR* argv[]) { // compiler error as no std ctor exists MyLogger logger; // calls parametrized ctor MyLogger logger1(R"(d:\test1.txt)"); logger1.WriteLine("Hello"); logger1.WriteLine("World"); // call parametrized ctor and move ctor // use exception handler try { MyLogger logger2 = MyLogger(R"(d:\test2.txt)"); logger2.WriteLine("Hello"); logger2.WriteLine("World"); } catch (std::ios_base::failure& e) { std::cout << e.what() << std::endl; return -1; } // move assignment MyLogger logger3 = MyLogger(R"(d:\test3.txt)"); logger3 = MyLogger(R"(d:\test4.txt)"); // calls move assignment operator logger3.WriteLine("Hello again"); // writes to test4.txt // copy ctor and copy assignment logger3 = MyLogger(logger1); // compiler error as copy ctor is deleted logger3 = logger1; // compiler error as copy assignment is deleted return 0; }
Multithreading
As the “WriteLine” function already uses locks it can be used within multithreading scenarios. The following source code shows an example with two threads using the same logger instance.
MyLogger gLogger1(R"(d:\test.txt)"); void DoSomething(std::string input) { //...do something //log function execution and results gLogger1.WriteLine("DoSomething was called with parameter: " + input); } void ExecuteThread(std::string threadNumber) { for (int i = 0; i < 10; i++) { DoSomething(threadNumber + "_" + std::to_string(i)); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } int _tmain(int argc, _TCHAR* argv[]) { DoSomething("in front of threads"); std::thread thread1(ExecuteThread, "1"); std::thread thread2(ExecuteThread, "2"); thread1.join(); thread2.join(); DoSomething("behind threads"); return 0; }
Singleton
If there should be one global logging instance which writes in a pre-defined file, you may use the singleton pattern to provide a global accessible logging instance. The following source code shows a possible implementation of the singleton pattern.
#include "MyLogger.h" namespace Logging { class MyLoggerSingleton { public: MyLoggerSingleton(MyLoggerSingleton const&) = delete; // Copy construct MyLoggerSingleton(MyLoggerSingleton&&) = delete; // Move construct MyLoggerSingleton& operator=(MyLoggerSingleton const&) = delete; // Copy assign MyLoggerSingleton& operator=(MyLoggerSingleton &&) = delete; // Move assign static MyLogger& Instance() { static MyLogger myInstance(R"(d:\test.txt)"); return myInstance; } protected: MyLoggerSingleton() {} ~MyLoggerSingleton() {} }; }
You can use this singleton within the client application.
void DoSomething(std::string input) { //...do something //log function execution and results MyLoggerSingleton::Instance().WriteLine("DoSomething was called with parameter: " + input); } void ExecuteThread(std::string threadNumber) { for (int i = 0; i < 10; i++) { DoSomething(threadNumber + "_" + std::to_string(i)); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } int _tmain(int argc, _TCHAR* argv[]) { DoSomething("in front of threads"); std::thread thread1(ExecuteThread, "1"); std::thread thread2(ExecuteThread, "2"); thread1.join(); thread2.join(); DoSomething("behind threads"); return 0; }
The singleton class is implemented by using a static member. This is a thread-safe implementation in C++11. I know you will find many excessive discussions about the singleton pattern, if it is thread-safe in c++ and whether you should use it at all. I don’t want to be part of this sometimes misleading discussions but I want to add some thoughts about the implemented singleton for the logger as you maybe want to implement something similar and may be confused whether a singleton is good or bad.
Singletons, in general, are widely used and they are one of the base software implementation patterns. They offer a lot of advantages. But of course, they will not match with any use case. For example, the logging approach shown in the implemented examples has one major disadvantage: the productive code is mixed up with logging code. If you have a complex application and the need for detailed logging in all situations, you code will be blown up very fast with a lot of logging code. In such cases it may be better to use other techniques, for example aspect oriented implementation.
Other discussion about thread safety of singletons and issues about a wrong clean up order of static variables are, in my opinion, outdated. With C++11 static member initialization is thread safe and since C++98 the cleanup order of statics is defined.
Summary
This article has shown some base implementation techniques like RAII, error handling, copy and movement ctor and singleton pattern. These techniques were used to implement a simple logging class. You may use this implementation as a template for implementing your own classes which have to manage a resource for example a file, a database connection or a network stream.