I started learning C++ recently for game development, and decided to write my own little library for debugging.
Here is the library (snoop.hpp)
#ifndef SNOOP_HPP#define SNOOP_HPP#include <chrono>#include <fstream>#include <iostream>#include <memory>#include <string>#include <mutex>#include <sstream>#include <functional>// Enumeration for different log levelsenum class LogLevel { FATAL, ERROR, WARN, INFO, DEBUG, VERB};// Forward declaration of Snoop classclass Snoop;// Type definition for custom error handler functionusing ErrorHandler = void (*)(const std::string&);// Type definition for custom log message formatter functionusing LogFormatter = std::function<std::string(LogLevel, const char*, int, const std::string&)>;class Snoop {public: // Constructor Snoop(LogLevel level = LogLevel::INFO, std::string filePath = "", bool log_to_console = true); // Destructor ~Snoop(); // Copy Constructor (deleted) Snoop(const Snoop&) = delete; // Copy Assignment Operator (deleted) Snoop& operator=(const Snoop&) = delete; // Move Constructor Snoop(Snoop&& other); // Move Assignment Operator Snoop& operator=(Snoop&& other); // Method to set a custom error handler void SetErrorHandler(ErrorHandler error_handler); // Method to set a custom log formatter void SetLogFormatter(LogFormatter formatter); // Public log methods for different log levels void Fatal(const char* file, int line, const std::string& message); void Error(const char* file, int line, const std::string& message); void Warn(const char* file, int line, const std::string& message); void Info(const char* file, int line, const std::string& message); void Debug(const char* file, int line, const std::string& message); void Verb(const char* file, int line, const std::string& message); // Methods to configure logging behavior void LogToFile(std::string filePath); void LogToConsole(bool log_to_console); void SetLevel(LogLevel level);private: LogLevel level = LogLevel::INFO; bool log_to_file = false; bool log_to_console = false; std::mutex mutex; std::unique_ptr<std::ofstream> file; ErrorHandler error_handler = nullptr; LogFormatter log_formatter = nullptr; // Helper function for resource transfer void swap(Snoop& other) noexcept; // Helper function for default message formatting std::string formatMessage(LogLevel level, const char* file, int line, const std::string& message); // Internal log function used by public log functions void log(LogLevel level, const char* file, int line, const std::string& message);};// Macros for simplified logging usage#define LOG_FATAL(instance, msg) instance.Fatal(__FILE__, __LINE__, msg)#define LOG_ERROR(instance, msg) instance.Error(__FILE__, __LINE__, msg)#define LOG_WARN(instance, msg) instance.Warn(__FILE__, __LINE__, msg)#define LOG_INFO(instance, msg) instance.Info(__FILE__, __LINE__, msg)#define LOG_DEBUG(instance, msg) instance.Debug(__FILE__, __LINE__, msg)#define LOG_VERB(instance, msg) instance.Verb(__FILE__, __LINE__, msg)// ConstructorSnoop::Snoop(LogLevel level, std::string filePath, bool log_to_console) { try { LogToFile(filePath); LogToConsole(log_to_console); SetLevel(level); } catch (const std::exception& e) { // Handle initialization exception if (this->error_handler) { this->error_handler("Exception during Snoop initialization: " + std::string(e.what())); } else { std::cerr << "Exception during Snoop initialization: " << e.what() << std::endl; } }}// DestructorSnoop::~Snoop() { // unique_ptr will automatically close the file when it goes out of scope}// Move ConstructorSnoop::Snoop(Snoop&& other) { // Transfer ownership of resources swap(other);}// Move Assignment OperatorSnoop& Snoop::operator=(Snoop&& other) { // Self-assignment check if (this != &other) { // Release current resources if (log_to_file) { file.reset(); // Close file by resetting unique_ptr } // Transfer ownership of resources from 'other' to 'this' swap(other); } return *this;}// Helper function to swap resourcesvoid Snoop::swap(Snoop& other) noexcept { using std::swap; // Ensure thread safety by locking both mutexes std::lock_guard<std::mutex> lock(mutex); std::lock_guard<std::mutex> otherLock(other.mutex); // Transfer resources between 'this' and 'other' swap(level, other.level); swap(log_to_file, other.log_to_file); swap(log_to_console, other.log_to_console); swap(file, other.file);}// Method to set a custom error handlervoid Snoop::SetErrorHandler(ErrorHandler error_handler) { this->error_handler = error_handler;}// Method to set a custom log formattervoid Snoop::SetLogFormatter(LogFormatter formatter) { this->log_formatter = formatter;}// Method to set the log levelvoid Snoop::SetLevel(LogLevel level) { this->level = level;}// Method to configure logging to consolevoid Snoop::LogToConsole(bool log_to_console) { this->log_to_console = log_to_console;}// Method to configure logging to filevoid Snoop::LogToFile(std::string filePath) { this->file.reset(); // Release previous file handle if (!filePath.empty()) { try { // Attempt to open the specified file for logging this->file = std::make_unique<std::ofstream>(filePath, std::ios_base::app); if (!(*this->file)) { // Error opening file if (this->error_handler) { this->error_handler("Failed to open file for logging: " + filePath); } else { std::cerr << "Error while opening log file: " << filePath << std::endl; } this->log_to_file = false; } else { // Successfully opened file this->log_to_file = true; } } catch (const std::exception& e) { // Handle exception while opening the log file if (this->error_handler) { this->error_handler("Exception while opening log file: " + std::string(e.what())); } else { std::cerr << "Exception while opening log file: " << e.what() << std::endl; } this->log_to_file = false; } } else { // Logging to file is disabled this->log_to_file = false; }}// Default message formatting functionstd::string Snoop::formatMessage(LogLevel level, const char* file, int line, const std::string& message) { std::ostringstream outstream; // Generate timestamp using clock = std::chrono::system_clock; auto now = clock::now(); auto seconds = std::chrono::time_point_cast<std::chrono::seconds>(now); auto fraction = now - seconds; std::time_t clock_now = clock::to_time_t(now); auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(fraction); char time_str[20]; std::strftime(time_str, sizeof(time_str), "%H:%M:%S", std::localtime(&clock_now)); outstream << time_str; // Add log level switch(level) { case LogLevel::FATAL: outstream << " [FTL] "; break; case LogLevel::ERROR: outstream << " [ERR] "; break; case LogLevel::WARN: outstream << " [WRN] "; break; case LogLevel::INFO: outstream << " [INF] "; break; case LogLevel::DEBUG: outstream << " [DBG] "; break; case LogLevel::VERB: outstream << " [VRB] "; break; } outstream << "[" << file << ":" << line << "] "; outstream << message; return outstream.str();}// Internal log functionvoid Snoop::log(LogLevel level, const char* file, int line, const std::string& message) { // Skip logging if the level is above the set logging level if (level > this->level) { return; } std::string outstring; // Format the log message using a custom formatter if provided if (this->log_formatter) { outstring = this->log_formatter(level, file, line, message); } else { // Use the default message formatting function outstring = formatMessage(level, file, line, message); } // Log to console if (this->log_to_console) { // Lock the mutex for thread safety while logging to console this->mutex.lock(); if (level == LogLevel::FATAL || level == LogLevel::ERROR) { std::clog << outstring << std::endl; } else { std::clog << outstring << "\n"; } this->mutex.unlock(); } // Log to file if (this->log_to_file) { // Lock the mutex for thread safety while logging to file this->mutex.lock(); if (level == LogLevel::FATAL || level == LogLevel::ERROR) { (*this->file) << outstring << std::endl; } else { (*this->file) << outstring << "\n"; } this->mutex.unlock(); }}// Public log functions that call the internal log functionvoid Snoop::Fatal(const char* file, int line, const std::string& message) { log(LogLevel::FATAL, file, line, message);}void Snoop::Error(const char* file, int line, const std::string& message) { log(LogLevel::ERROR, file, line, message);}void Snoop::Warn(const char* file, int line, const std::string& message) { log(LogLevel::WARN, file, line, message);}void Snoop::Info(const char* file, int line, const std::string& message) { log(LogLevel::INFO, file, line, message);}void Snoop::Debug(const char* file, int line, const std::string& message) { log(LogLevel::DEBUG, file, line, message);}void Snoop::Verb(const char* file, int line, const std::string& message) { log(LogLevel::VERB, file, line, message);}#endifAnd here I have a file that showcases how to use it (example.cpp):
#include "snoop.hpp"int main() { // Create a Snoop logger instance with INFO level Snoop logger(LogLevel::INFO); // Set a custom error handler logger.SetErrorHandler([](const std::string& error) { std::cerr << "Custom Error Handler: " << error << std::endl; }); // Log messages at different levels with the default formatter LOG_FATAL(logger, "This is a fatal message."); LOG_ERROR(logger, "This is an error message."); LOG_WARN(logger, "This is a warning message."); LOG_INFO(logger, "This is an info message."); LOG_DEBUG(logger, "This is a debug message."); // Gets skipped LOG_VERB(logger, "This is a verbose message."); // Gets skipped // Set a custom log formatter logger.SetLogFormatter([](LogLevel level, const char* file, int line, const std::string& message) -> std::string { std::ostringstream formatted; formatted << "<" << file << "::" << line << "> "; switch (level) { case LogLevel::FATAL: formatted << "[FATAL] "; break; case LogLevel::ERROR: formatted << "[ERROR] "; break; case LogLevel::WARN: formatted << "[WARN] "; break; case LogLevel::INFO: formatted << "[INFO] "; break; case LogLevel::DEBUG: formatted << "[DEBUG] "; break; case LogLevel::VERB: formatted << "[VERB] "; break; } formatted << message; return formatted.str(); }); // Change LogLevel to verbose logger.SetLevel(LogLevel::VERB); // Configure logger to log to a file logger.LogToFile("./log/file.log"); // Throws error (directory doesn't exist) // Log messages at different levels with the custom formatter LOG_FATAL(logger, "This is a fatal message."); LOG_ERROR(logger, "This is an error message."); LOG_WARN(logger, "This is a warning message."); LOG_INFO(logger, "This is an info message."); LOG_DEBUG(logger, "This is a debug message."); LOG_VERB(logger, "This is a verbose message."); // Configure logger to log to a file ONLY logger.LogToFile("./example.log"); logger.LogToConsole(false); // Log additional messages after configuring file logging LOG_FATAL(logger, "This is a fatal message (logged to file)."); LOG_ERROR(logger, "This is an error message (logged to file)."); LOG_WARN(logger, "This is a warning message (logged to file)."); LOG_INFO(logger, "This is an info message (logged to file)."); LOG_DEBUG(logger, "This is a debug message (logged to file)."); LOG_VERB(logger, "This is a verbose message (logged to file)."); return 0;}Anything I could change or add to make my code more safe and/or performant? General C++ advice is welcome too! Thanks :)
Edit: I applied the excellent advice given by G. Sliepen; you can see the updated version here:https://github.com/makeyen/snoop
1 Answer1
Unnecessary things
- You don't need to forward declare
class Snoop, nothing uses it before you actually define the class. this->is almost never necessary in C++. The only issue is withlevelinSnoop::log(); this can be solved by renaming either the function parameter or the member variable namedlevel. I suggest renaming the latter tomaximum_level.- The destructor is empty, you can omit its definition entirely.
- Catching errors in the constructor:
error_handleris never set in the constructor, so it's useless to check for it. It's also better to just let the exception propagate than to print an error messageand then to ignore the error. - There is no need to delete the copy constructor and assignment operator explicitly; since the member variable
mutexis non-copyable, the whole class will be non-copyable by default as well. - Using a
std::unique_ptrto hold thestd::ofstream: you can default-construct astd::ofstreamthat is not associated with any file, and you canstd::move()andstd::swap()std::ofstreams just like you can withstd::mutex.
Simplify the code
There is quite some complexity inlog() because it can choose between custom formatters or a default one, and can log to a file and/or the console. I would also move the early checks for the log level into the class definition so it can be inlined. And together with the use ofstd::lock_guard and some helper functions, it can be rewritten like so:
class Snoop {public: … void log(LogLevel level, …) { if (level <= maximum_level) log_internal(level, …); } void Fatal(…) { log(LogLevel::FATAL, …); } …private: … std::ofstream file; void log_internal(LogLevel level, const char* file, int line, const std::string& message);};static void log_formatted(bool enabled, std::ofstream& file, LogLevel level, const std::string& formatted_message) { if (enabled) { file << formatted_message << '\n'; if (LogLevel::FATAL || level == LogLevel::ERROR) { file.flush(); } }}void Snoop::log_internal(LogLevel level, const char* file, int line, const std::string& message) { std::string outstring = formatMessage(level, file, line, message); std::lock_guard lock(mutex); log_formatted(log_to_console, clog, level, outstring); log_formatted(log_to_file, file, level, outstring);}std::string Snoop::formatMessage(LogLevel level, const char* file, int line, const std::string& message) { if (log_formatter) { return log_formatter(level, file, line, message); } …}The macros can be avoided in C++20
C++20 addedstd::source_location, which gives you the filename and line number in a way that avoids having to use macros.
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.