Writing a simple logging function in C++20

Logging is an important feature of any serious program. In general, one should use established libraries for logging. However, for the sake of showcasing several new features from C++20, now available in Visual Studio 2019 16.10, I will show, in this post, how one can quickly write a simple logging function.

What I am interested in logging is the following:

  • the type of the log message, such as error, warning, or information
  • the local time when the message was logged
  • the source location with filename, function name, and source code line where the logging occurred
  • the message

To get this done, I’ll use the following C++ 20 featurse:

The end result is should be looking like the following:

[I] 2021-07-03 09:07:15.5349107 GMT+3 | cpp_test.cpp:main:51 | Logging from main!
[E] 2021-07-03 09:07:15.5477301 GMT+3 | cpp_test.cpp:execute:46 | Error in execute!

Let’s take all these features mentioned previously one at a time. We’ll start with the type of the message. For this, I’ll simply define a scoped enum as follows:

enum class log_level : char
{
   Info = 'I',
   Warning = 'W',
   Error = 'E'
};

Next, is the time when the event that’s being logged occurred. The current time can be retried with std::chrono::system_clock::now(). The value returned by this call is a std::chrono::system_clock::time_point. However, this is a Coordinated Universal Time (UTC) value, and I specified that I want to display the local time. This can be done easily, with two C++20 features:

  • std::chrono::zoned_time, a class that represents a pairing between a time point and a time zone, enabling us to display the time within a specified time zone
  • std::chrono::current_zone(), which is a function that returns the current time zone (from the time zone database).

We can use these to convert from UTC to the local time zone as follows:

auto as_local(std::chrono::system_clock::time_point const tp)
{
   return std::chrono::zoned_time{ std::chrono::current_zone(), tp };
}

The zoned_time value can be converted to a string using the new text formatting library and the std::format() function.

std::string to_string(auto tp)
{
   return std::format("{:%F %T %Z}", tp);
}

In this snippet, %F, %T, and %Z are specifiers that indicate how the time point will be formatted. The full list of standard specifiers for chrono is available here. For basic and string types, the standard specifiers are available here.

Next on the list is the source code location. The C++20 class std::source_location contains information about the file name (the file_name() function), line (the line() function), column (the column() function), and function name (the function_name() function) . The static current() function returns an object of this type initialized with information about the location of the call site.

According to the documentation available at cppreference, the file_name() function returns the name of the file (such as main.cpp), and function_name() the full signature of the function, such as int main(int, const char* const*) or S& S::operator=(const S&). However, the Visual Studio 2019 16.10 implementation, that I’m using differs, such that file_name() returns the full path of the file and function_name() only the name of the function.

To print the source location information, we can use the following function that returns a string formatted with std::format(). Notice that I used std::filesystem::path to extract only the file name from the path returned by file_name().

std::string to_string(std::source_location const source)
{
   return std::format("{}:{}:{}", 
      std::filesystem::path(source.file_name()).filename().string(),
      source.function_name(),
      source.line());
}

Last but not least, is the logging function that looks as follows:

void log(log_level const level, 
         std::string_view const message, 
         std::source_location const source = std::source_location::current())
{
   std::cout
      << std::format("[{}] {} | {} | {}", 
                     static_cast<char>(level), 
                     to_string(as_local(std::chrono::system_clock::now())), 
                     to_string(source), 
                     message)
      << '\n';
}

This function takes three arguments: the log level, the message to be logged, and a source location object constructed with a call to std::source_location::current() (which is a consteval function, meaning it’s an immediate function that produces a compile-time constant value).

With all these in place, we can call the log() function from our code as follows:

void execute(int, double)
{
   log(log_level::Error, "Error in execute!");
}

int main()
{
   log(log_level::Info, "Logging from main!");
   execute(0, 0);
}

Of course, these new features showcased here (especially the text formatting and the calendar and time zone extensions) are more complex, provide more features, and require more study. However, this small example should be a good example how these new features can be used together to help us simplify our code.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.