Complex Logging and Debugging in C++

Common tools and new C++20 features

CMP
4 min readOct 26, 2023

When developing complex C++ programs, you will often run into complex issues that are difficult to debug just by looking at the source code. This article introduces various logging and debugging tools and strategies that can be used to analyze these complex bugs.

Debugging Tools

Debugging is hard. It is a difficult skill to master, but it is essential to at least be comfortable with the basics of a standard debugger. GDB is a popular debugger that works best on Unix-based systems, whereas WinDBG is a good option for Windows. These tools allow you to set breakpoints and step through your code line-by-line, as well as see the internals of your program such as local variables, memory registers, stack traces, etc.

For memory-related issues, such as memory leaks or crashes due to invalid memory accesses, Valgrind is a great tool to use alongside GDB. If using WinDBG on Windows, the !analyze -v command will help analyze these types of crashes.

Before doing any of this though, I recommend using a static analysis tool to identify potential issues in your code before runtime. Your compiler will check for syntax issues that will prevent the compilation of your code, but static analysis tools will further analyze your code to detect common bugs beyond syntax problems. Popular static analysis tools include clang-tidy and the open source, cross-platform cppcheck.

Logging Libraries

Simple, consistent bugs can be debugged using primitive print statements without much hassle (e.g. std::cout << "I'm here!" << std::endl;). However, this is not a great long-term solution for intermittent bugs that only occur in specific scenarios or under specific conditions, such as data races caused by improper synchronization in fluctuation timing environments.

Although print statements can be used as “logs”, it is often a better idea to use a dedicated logging library that supports various log levels and can write to a log file. Common logging libraries for C++ include plog, spdlog, and Boost.Log. Additionally, you can write your own logging library to support your individual needs.

In C++20, the <source_location> header was added alongside std::source_location. This provides information about source code, which can be used in your logs. Look at the following example:

#include <iostream>
#include <source_location>

void logMessage(const std::string& message, const std::source_location& loc = std::source_location::current()) {
std::cout << "Message: " << message << std::endl;
std::cout << "File: " << loc.file_name() << std::endl;
std::cout << "Line: " << loc.line() << std::endl;
std::cout << "Function: " << loc.function_name() << std::endl;
std::cout << "Column: " << loc.column() << std::endl;
}

int main() {
logMessage("Hello world!");
return 0;
}

Here, the logMessage function takes in a log message string and a std::source_location as parameters, defaulting to using the current source location (std::source_location::current()). The output of the example above is shown below:

Message: Hello world!
File: example.cpp
Line: 13
Function: main
Column: 3

Additionally, C++20 also introduce the <format> header for simplified formatting of print statements, similar to printf in C. Below is a basic example:

int main() {
int a = 5;
double b = 10.1234;
std::cout << std::format("a = {}, b = {:.2f}", a, b) << std::endl;
return 0;
}
a = 5, b = 10.12

Logging Strategies

For the most effective logs, you must establish a set of logging levels. At the bare minimum, you should have an ERROR level and an INFO level. However, most logging libraries will contain a few more levels, shown below in order of most to least granularity:

VERBOSE
DEBUG
INFO
WARNING
ERROR
FATAL
  • FATAL log messages are uncommon and represent catastrophic errors.
  • ERROR messages represent errors that cause some functionality to fail but aren’t as severe as FATAL messages.
  • WARNING messages represent unexpected behavior that does not cause functionality to fail.
  • INFO messages are very common and are used to log when an event happens or to log any other useful information. These are very common.
  • DEBUG messages are used when events occur that are useful for software debugging and where finer granularity is needed.
  • VERBOSE messages are reserved for the finest granular messages that can be ignored in most cases but may be useful in extensive debugging sessions.

Ideally, logs should be collected conditionally based on highest log level. This means that when you run your program, you choose the highest level of logs collected — In a standard run you may collect up to the INFO level, but when bugs are found and you are debugging, you may collect up to DEBUG or VERBOSE levels. It is also generally a good idea to log to multiple outputs, such as the console and to a log file.

Conclusion

There is a variety of logging and debugging tools and techniques available to you when developing a program in C++. It is generally recommended to use a static analysis tool to catch common bugs in your code, a standard debugger like GDB, and a memory debugging tool like Valgrind. When adding logs for your program, using an extensive logging library with log levels and multiple output streams like spdlog is recommended. If your program has specialized needs, then creating your own logging library and incorporating new C++20 features like std::source_location and std::format is also an option.

--

--

CMP

Software engineer specializing in operating systems, navigating the intracicies of the C++ language and systems programming.