In C++ programming, debug logs are crucial to locating problems and optimizing code. Effective debug logs not only help us quickly locate errors, but also provide valuable information about the running status of the program. This article will introduce several commonly used debug log output methods and teach you how to add timestamps to the log.
1. Use the #ifdef _DEBUG macro
In C++, one of the commonly used methods is to use conditional compilation macros, and control log output is enabled only in debug mode. This method is very simple and does not affect the performance of the release version, because log macros are removed in the release version.
#include <iostream> #ifdef _DEBUG #define LOG_ERROR(msg) \ std::cerr << "[ERROR] " << __FILE__ << ":" << __LINE__ << " (" << __FUNCTION__ << ") - " << msg << std::endl; #define LOG_DEBUG(msg) \ std::cout << "[DEBUG] " << __FILE__ << ":" << __LINE__ << " (" << __FUNCTION__ << ") - " << msg << std::endl; #else #define LOG_ERROR(msg) #define LOG_DEBUG(msg) #endif
explain:
- _DEBUG macro: This macro is automatically defined in debug mode. Through it, we can control log output to be enabled only during debugging.
- LOG_DEBUG macro: It prints the current file name, line number, function name, and incoming debug information. If it is a release version, this macro will be ignored.
advantage:
- Detailed information can be provided during debugging.
- The performance of the release version will not be affected, because the macro will be completely removed when released.
shortcoming:
- The use of macros in complex projects may lead to excessive debugging information, especially when the log volume is high, which may affect performance.
- Macros cannot catch exceptions or provide advanced logging functions (such as log level, asynchronous processing, etc.).
2. Add timestamp: accurate to milliseconds
In order to further improve the usefulness of the log, we can add timestamps to the log, which is very helpful for debugging complex asynchronous operations, performance bottlenecks and other problems. C++11 introduces a library that allows us to record time accurately to milliseconds.
#include <iostream> #include <chrono> #include <iomanip> #ifdef _DEBUG #define LOG_DEBUG(msg) { \ auto now = std::chrono::system_clock::now(); \ auto duration = now.time_since_epoch(); \ auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count(); \ std::time_t time_now = std::chrono::system_clock::to_time_t(now); \ std::tm time_tm = *std::localtime(&time_now); \ std::cout << "[" << std::put_time(&time_tm, "%Y-%m-%d %H:%M:%S") << "." << std::setw(3) << std::setfill('0') << (milliseconds % 1000) << "] " \ << "[DEBUG] " << __FILE__ << ":" << __LINE__ << " (" << __FUNCTION__ << ") - " << msg << std::endl; \ } #else #define LOG_DEBUG(msg) #endif
explain:
Get the current time:
- Use std::chrono::system_clock::now() to get the current system time.
- Use std::chrono::duration_cast to accurately count the time to milliseconds and calculate the number of milliseconds since the epoch.
Format timestamp:
- Convert time to type std::time_t, and then convert it to std::tm structure via std::localtime.
- Use std::put_time to format std::tm into HH:MM:SS format.
- The milliseconds part is calculated and formatted into three digits by milliseconds %1000.
Output format:
- The timestamp format is [%Y-%m-%d %H:%M:%S], for example 2025-01-18 17:52:59.489.
- The file name, line number, function name and debug information will be displayed in the log.
example:
int main() { LOG_DEBUG("This is a debug message with timestamp!"); return 0; }
Output (assuming the current time is 14:30:45.123):
[2025-01-18 17:52:59.489] [DEBUG] :10 (main) - This is a debug message with timestamp!
and debug logging methods in MFC
In addition to standard C++ methods, Windows and MFC also provide some built-in debug logging tools that can help developers get richer information during the debugging process.
MFC debug macros
In MFC, there are several commonly used macros that can help us debug log output:
TRACE: Used to print debug information to the output window, similar to printf, but output to Visual Studio's debug output window.
TRACE("Code:%d\n", nCode);
ASSERT: Used to verify the condition. If the condition is false, an assertion dialog box will pop up, displaying the error file and line number.
ASSERT(n > 0); // If n <= 0, an assertion dialog box will pop up
AfxMessageBox: A message box pops up, displaying debug information, which is usually used to show errors or prompts to the user during debugging.
AfxMessageBox(_T("This is a message box"));
Windows API debugging functions
OutputDebugString: This function can output debugging information to the debugger output window.
OutputDebugString(_T("This is a debug string"));
DbgPrint: In Windows driver development, DbgPrint is used to send information to debug output, suitable for driver development.
DbgPrint("This is a debug message\n");
ASSERT Macro
The Windows API also provides the ASSERT macro, which is similar to ASSERT in MFC, for checking conditions and interrupting programs when conditions fail.
ASSERT(n > 0); // If the conditions do not hold, a debug dialog box will pop up
4. Logger Class
You can create a log class to encapsulate the output of the log. In this way, you can centrally manage the format, log level, and output destination (console, file, etc.).
#include <iostream> #include <fstream> #include <string> class Logger { public: enum LogLevel { INFO, WARNING, ERROR, DEBUG }; Logger(LogLevel level = INFO) : logLevel(level) {} void log(LogLevel level, const std::string& msg) { if (level >= logLevel) { std::cout << "[" << levelToString(level) << "] " << msg << std::endl; } } private: LogLevel logLevel; std::string levelToString(LogLevel level) { switch (level) { case INFO: return "INFO"; case WARNING: return "WARNING"; case ERROR: return "ERROR"; case DEBUG: return "DEBUG"; default: return "UNKNOWN"; } } }; #define LOG(level, msg) Logger().log(level, msg)
advantage:
- Supports multi-level logging (such as INFO, WARNING, ERROR, DEBUG).
- It is easier to scale and can output logs to files, databases, etc.
- Convenient to control the content and level of log output.
shortcoming:
- Objects or static methods need to be created, which may affect performance.
- Configuration and management are complex.
5. Third-party log library: spdlog
For more complex logging needs, third-party libraries such as spdlog provide rich functions, such as support for multi-level logs, asynchronous logs, file rotation, etc. Here is a simple example of using spdlog to output a timestamped log:
#include <spdlog/> #define LOG_DEBUG(msg) spdlog::debug("[DEBUG] {}:{} ({}) - {}", __FILE__, __LINE__, __FUNCTION__, msg) #define LOG_ERROR(msg) spdlog::error("[ERROR] {}:{} ({}) - {}", __FILE__, __LINE__, __FUNCTION__, msg) int main() { spdlog::set_level(spdlog::level::debug); // Set global log level LOG_DEBUG("This is a debug message."); LOG_ERROR("This is an error message."); }
spdlog will automatically timestamp each log and support rich output formats and multiple output methods (such as files, terminals, log servers, etc.).
6. Log file output
If you need to write the logs to a file, directly redirecting the output stream is an easy way. It can be compiled in combination with conditionals, log classes or external libraries.
#include <iostream> #include <fstream> #define LOG_TO_FILE(msg) { \ std::ofstream logFile("", std::ios::app); \ logFile << "[INFO] " << __FILE__ << ":" << __LINE__ << " (" << __FUNCTION__ << ") - " << msg << std::endl; \ } int main() { LOG_TO_FILE("This is a log message."); }
advantage:
- It can persist log data for easy post-view and analysis.
- Flexible configuration of console and file output.
shortcoming:
It has a certain impact on performance, especially when writing to files.
There are no advanced features such as log level, filtering and formatting.
7. Log file rotation
If the log file is too large, the file rotation function can be realized, that is, it will automatically switch to a new file after it exceeds a certain size. This is usually done through log libraries (such as spdlog) or by itself.
#include <iostream> #include <fstream> #define LOG_ROTATE_FILE(msg) { \ static int count = 0; \ std::ofstream logFile("log_" + std::to_string(count) + ".txt", std::ios::app); \ logFile << "[INFO] " << msg << std::endl; \ if (++count >= 10) count = 0; \ } int main() { for (int i = 0; i < 15; ++i) { LOG_ROTATE_FILE("Log message number " + std::to_string(i)); } }
advantage:
- Automatically manage the size of log files to avoid excessively large log files.
- File rotation can effectively manage logs.
shortcoming:
Additional logic is required to handle log switching and naming.
Summarize
In C++ development, debug logs are an important tool for debugging and optimizing code. By using conditional compilation macros, std::chrono to accurately record timestamps, we can add useful context information to the debug log to help us quickly locate problems. In Windows and MFC environments, built-in debugging tools such as TRACE, ASSERT and OutputDebugString can also provide us with convenient debugging information. In addition, third-party logging libraries such as spdlog provide more features for complex projects that require efficient, asynchronous logging.
This is the end of this article about implementing debug log output in C++. For more related C++ debug log output content, please search for my previous article or continue browsing the related articles below. I hope everyone will support me in the future!