Book Image

Advanced C++

By : Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price
5 (1)
Book Image

Advanced C++

5 (1)
By: Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price

Overview of this book

C++ is one of the most widely used programming languages and is applied in a variety of domains, right from gaming to graphical user interface (GUI) programming and even operating systems. If you're looking to expand your career opportunities, mastering the advanced features of C++ is key. The book begins with advanced C++ concepts by helping you decipher the sophisticated C++ type system and understand how various stages of compilation convert source code to object code. You'll then learn how to recognize the tools that need to be used in order to control the flow of execution, capture data, and pass data around. By creating small models, you'll even discover how to use advanced lambdas and captures and express common API design patterns in C++. As you cover later chapters, you'll explore ways to optimize your code by learning about memory alignment, cache access, and the time a program takes to run. The concluding chapter will help you to maximize performance by understanding modern CPU branch prediction and how to make your code cache-friendly. By the end of this book, you'll have developed programming skills that will set you apart from other C++ programmers.
Table of Contents (11 chapters)
7
6. Streams and I/O

Chapter 6 – Streams and I/O

Activity 1 The Logging System for The Art Gallery Simulator

The thread-safe logger allows us to output data to the Terminal simultaneously. We implement this logger by inheriting from the std::ostringstream class and using a mutex for synchronization. We will implement a class that provides an interface for the formatted output and our logger will use it to extend the basic output. We define macro definitions for different logging levels to provide an interface that will be easy and clear to use. Follow these steps to complete this activity:

  1. Open the project from Lesson6.
  2. Create a new directory called logger inside the src/ directory. You will get the following hierarchy:
    Figure 6.25: The hierarchy of the project
    Figure 6.25: The hierarchy of the project
  3. Create a header and source file called LoggerUtils. In LoggerUtils.hpp, add include guards. Include the <string> header to add support for working with strings. Define a namespace called logger and then define a nesting namespace called utils. In the utils namespace, declare the LoggerUtils class.
  4. In the public section, declare the following static functions: getDateTime, getThreadId, getLoggingLevel, getFileAndLine, getFuncName, getInFuncName, and getOutFuncName. Your class should look as follows:

    #ifndef LOGGERUTILS_HPP_

    #define LOGGERUTILS_HPP_

    #include <string>

    namespace logger

    {

    namespace utils

    {

    class LoggerUtils

    {

    public:

         static std::string getDateTime();

         static std::string getThreadId();

         static std::string getLoggingLevel(const std::string& level);

         static std::string getFileAndLine(const std::string& file, const int& line);

         static std::string getFuncName(const std::string& func);

         static std::string getInFuncName(const std::string& func);

         static std::string getOutFuncName(const std::string& func);

    };

    } // namespace utils

    } // namespace logger

    #endif /* LOGGERUTILS_HPP_ */

  5. In LoggerUtils.cpp, add the required includes: the "LoggerUtils.hpp" header, <sstream> for std::stringstream support, and <ctime> for date and time support:

    #include "LoggerUtils.hpp"

    #include <sstream>

    #include <ctime>

    #include <thread>

  6. Enter the logger and utils namespaces. Write the required function definitions. In the getDateTime() function, get the local time using the localtime() function. Format it into a string using the strftime() function. Convert it into the desired format using std::stringstream:

    std::string LoggerUtils::getDateTime()

    {

         time_t rawtime;

         struct tm * timeinfo;

         char buffer[80];

         time (&rawtime);

         timeinfo = localtime(&rawtime);

         strftime(buffer,sizeof(buffer),"%d-%m-%YT%H:%M:%S",timeinfo);

         std::stringstream ss;

         ss << "[";

         ss << buffer;

         ss << "]";

         return ss.str();

    }

  7. In the getThreadId() function, get the current thread ID and convert it into the desired format using std::stringstream:

    std::string LoggerUtils::getThreadId()

    {

         std::stringstream ss;

         ss << "[";

         ss << std::this_thread::get_id();

         ss << "]";

         return ss.str();

    }

  8. In the getLoggingLevel() function, convert the given string into the desired format using std::stringstream:

    std::string LoggerUtils::getLoggingLevel(const std::string& level)

    {

         std::stringstream ss;

         ss << "[";

         ss << level;

         ss << "]";

         return ss.str();

    }

  9. In the getFileAndLine() function, convert the given file and line into the desired format using std::stringstream:

    std::string LoggerUtils::getFileAndLine(const std::string& file, const int& line)

    {

         std::stringstream ss;

         ss << " ";

         ss << file;

         ss << ":";

         ss << line;

         ss << ":";

         return ss.str();

    }

  10. In the getFuncName() function, convert the function name into the desired format using std::stringstream:

    std::string LoggerUtils::getFuncName(const std::string& func)

    {

         std::stringstream ss;

         ss << " --- ";

         ss << func;

         ss << "()";

         return ss.str();

    }

  11. In the getInFuncName() function convert the function name to the desired format using std::stringstream.

    std::string LoggerUtils::getInFuncName(const std::string& func)

    {

         std::stringstream ss;

         ss << " --> ";

         ss << func;

         ss << "()";

         return ss.str();

    }

  12. In the getOutFuncName() function, convert the function name into the desired format using std::stringstream:

    std::string LoggerUtils::getOutFuncName(const std::string& func)

    {

         std::stringstream ss;

         ss << " <-- ";

         ss << func;

         ss << "()";

         return ss.str();

    }

  13. Create a header file called LoggerMacroses.hpp. Add include guards. Create macro definitions for each LoggerUtils function: DATETIME for the getDateTime() function, THREAD_ID for the getThreadId() function, LOG_LEVEL for the getLoggingLevel() function, FILE_LINE for the getFileAndLine() function, FUNC_NAME for the getFuncName() function, FUNC_ENTRY_NAME for the getInFuncName() function, and FUNC_EXIT_NAME for the getOutFuncName() function. As a result, the header file should look as follows:

    #ifndef LOGGERMACROSES_HPP_

    #define LOGGERMACROSES_HPP_

    #define DATETIME \

         logger::utils::LoggerUtils::getDateTime()

    #define THREAD_ID \

         logger::utils::LoggerUtils::getThreadId()

    #define LOG_LEVEL( level ) \

         logger::utils::LoggerUtils::getLoggingLevel(level)

    #define FILE_LINE \

         logger::utils::LoggerUtils::getFileAndLine(__FILE__, __LINE__)

    #define FUNC_NAME \

         logger::utils::LoggerUtils::getFuncName(__FUNCTION__)

    #define FUNC_ENTRY_NAME \

         logger::utils::LoggerUtils::getInFuncName(__FUNCTION__)

    #define FUNC_EXIT_NAME \

         logger::utils::LoggerUtils::getOutFuncName(__FUNCTION__)

    #endif /* LOGGERMACROSES_HPP_ */

  14. Create a header and source file called StreamLogger. In StreamLogger.hpp, add the required include guards. Include the LoggerMacroses.hpp and LoggerUtils.hpp header files. Then, include the <sstream> header for std::ostringstream support, the <thread> header for std::thread support, and the <mutex> header for std::mutex support:

    #include "LoggerMacroses.hpp"

    #include "LoggerUtils.hpp"

    #include <sstream>

    #include <thread>

    #include <mutex>

  15. Enter the namespace logger. Declare the StreamLogger class, which inherits from the std::ostringstream class. This inheritance allows us to use an overloaded left shift operator, <<, for logging. We don't set the output device, so the output will not be performed – just stored in the internal buffer. In the private section, declare a static std::mutex variable called m_mux. Declare constant strings so that you can store the logging level, file and line, and function name. In the public section, declare a constructor that takes the logging level, file and line, and function name as parameters. Declare a class destructor. The class declaration should look like as follows:

    namespace logger

    {

    class StreamLogger : public std::ostringstream

    {

    public:

         StreamLogger(const std::string logLevel,

                      const std::string fileLine,

                      const std::string funcName);

         ~StreamLogger();

    private:

         static std::mutex m_mux;

         const std::string m_logLevel;

         const std::string m_fileLine;

         const std::string m_funcName;

    };

    } // namespace logger

  16. In StreamLogger.cpp, include the StreamLogger.hpp and <iostream> headers for std::cout support. Enter the logger namespace. Define the constructor and initialize all the members in the initializer list. Then, define the destructor and enter its scope. Lock the m_mux mutex. If the internal buffer is empty, output only the date and time, thread ID, logging level, file and line, and the function name. As a result, we will get the line in the following format: [dateTtime][threadId][logLevel][file:line: ][name() --- ]. If the internal buffer contains any data, output the same string with the buffer at the end. As a result, we will get the line in the following format: [dateTtime][threadId][logLevel][file:line: ][name() --- ] | message. The complete source file should look as follows:

    #include "StreamLogger.hpp"

    #include <iostream>

    std::mutex logger::StreamLogger::m_mux;

    namespace logger

    {

    StreamLogger::StreamLogger(const std::string logLevel,

                      const std::string fileLine,

                      const std::string funcName)

              : m_logLevel(logLevel)

              , m_fileLine(fileLine)

              , m_funcName(funcName)

    {}

    StreamLogger::~StreamLogger()

    {

         std::lock_guard<std::mutex> lock(m_mux);

         if (this->str().empty())

         {

              std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << std::endl;

         }

         else

         {

              std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << " | " << this->str() << std::endl;

         }

    }

    }

  17. Create a header file called Logger.hpp and add the required include guards. Include the StreamLogger.hpp and LoggerMacroses.hpp headers. Next, create the macro definitions for the different logging levels: LOG_TRACE(), LOG_DEBUG(), LOG_WARN(), LOG_TRACE(), LOG_INFO(), LOG_ERROR(), LOG_TRACE_ENTRY(), and LOG_TRACE_EXIT().The complete header file should look as follows:

    #ifndef LOGGER_HPP_

    #define LOGGER_HPP_

    #include "StreamLogger.hpp"

    #include "LoggerMacroses.hpp"

    #define LOG_TRACE() logger::StreamLogger{LOG_LEVEL("Trace"), FILE_LINE, FUNC_NAME}

    #define LOG_DEBUG() logger::StreamLogger{LOG_LEVEL("Debug"), FILE_LINE, FUNC_NAME}

    #define LOG_WARN() logger::StreamLogger{LOG_LEVEL("Warning"), FILE_LINE, FUNC_NAME}

    #define LOG_TRACE() logger::StreamLogger{LOG_LEVEL("Trace"), FILE_LINE, FUNC_NAME}

    #define LOG_INFO() logger::StreamLogger{LOG_LEVEL("Info"), FILE_LINE, FUNC_NAME}

    #define LOG_ERROR() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_NAME}

    #define LOG_TRACE_ENTRY() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_ENTRY_NAME}

    #define LOG_TRACE_EXIT() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_EXIT_NAME}

    #endif /* LOGGER_HPP_ */

  18. Replace all the std::cout calls with the appropriate macro definition call. Include the logger/Logger.hpp header in the Watchman.cpp source file. In the runAdd() function, replace all instances of std::cout with macro definitions for different logging levels. The runAdd() function should look as follows:

    void Watchman::runAdd()

    {

         while (true)

         {

              std::unique_lock<std::mutex> locker(m_AddMux);

              while(!m_AddNotified)

              {

                   LOG_DEBUG() << "Spurious awakening";

                   m_CondVarAddPerson.wait(locker);

              }

              LOG_INFO() << "New person came";

              m_AddNotified = false;

              while (m_CreatedPeople.size() > 0)

              {

                   try

                   {

                        auto person = m_CreatedPeople.get();

                        if (m_PeopleInside.size() < CountPeopleInside)

                        {

                             LOG_INFO() << "Welcome in the our Art Gallery";

                             m_PeopleInside.add(std::move(person));

                        }

                        else

                        {

                             LOG_INFO() << "Sorry, we are full. Please wait";

                             m_PeopleInQueue.add(std::move(person));

                        }

                   }

                   catch(const std::string& e)

                   {

                        LOG_ERROR() << e;

                   }

              }

              LOG_TRACE() << "Check people in queue";

              if (m_PeopleInQueue.size() > 0)

              {

                   while (m_PeopleInside.size() < CountPeopleInside)

                   {

                        try

                        {

                             auto person = m_PeopleInQueue.get();

                             LOG_INFO() << "Welcome in the our Art Gallery";

                             m_PeopleInside.add(std::move(person));

                        }

                        catch(const std::string& e)

                        {

                             LOG_ERROR() << e;

                        }

                   }

              }

         }

    }

  19. Notice how we use our new logger. We invoke the macro definition with parentheses and use the left shift operator:

    LOG_ERROR() << e;

    Or

    LOG_INFO() << "Welcome in the our Art Gallery";

  20. Do the same replacement for the rest of code.
  21. Build and run the application. In the Terminal, you will see that log messages appear from the different threads with different logging levels and with useful information. After some time has passed, you will get some output similar to the following:
Figure 6.26: The execution result of the activity project
Figure 6.26: The execution result of the activity project

As you can see, it's really easy to read and understand logs. You can easily change the StreamLogger class to write logs to the file on the filesystem if your needs differ. You can add any other information that you may need to debug your application using logs, such as output function parameters. You can also override the left shift operator for your custom types to output debug information easily.

In this project, we employed many things that we have learned about during this chapter. We created an additional stream for thread-safe output, we formatted the output to the desired representation, we employed std::stringstream to perform formatting data, and we used macro definitions for convenient logger usage. Thus, this project demonstrates our skills in working with concurrent I/O.