logger class in C++

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.

Werbung
Dieser Beitrag wurde unter C++ veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit deinem WordPress.com-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s