4
\$\begingroup\$

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 levels
enum class LogLevel {
 FATAL,
 ERROR,
 WARN,
 INFO,
 DEBUG,
 VERB
};
// Forward declaration of Snoop class
class Snoop;
// Type definition for custom error handler function
using ErrorHandler = void (*)(const std::string&);
// Type definition for custom log message formatter function
using 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)
// Constructor
Snoop::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;
 }
 }
}
// Destructor
Snoop::~Snoop() {
 // unique_ptr will automatically close the file when it goes out of scope
}
// Move Constructor
Snoop::Snoop(Snoop&& other) {
 // Transfer ownership of resources
 swap(other);
}
// Move Assignment Operator
Snoop& 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 resources
void 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 handler
void Snoop::SetErrorHandler(ErrorHandler error_handler) {
 this->error_handler = error_handler;
}
// Method to set a custom log formatter
void Snoop::SetLogFormatter(LogFormatter formatter) {
 this->log_formatter = formatter;
}
// Method to set the log level
void Snoop::SetLevel(LogLevel level) {
 this->level = level;
}
// Method to configure logging to console
void Snoop::LogToConsole(bool log_to_console) {
 this->log_to_console = log_to_console;
}
// Method to configure logging to file
void 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 function
std::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 function
void 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 function
void 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);
}
#endif

And 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

asked Aug 10, 2023 at 11:01
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

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 with level in Snoop::log(); this can be solved by renaming either the function parameter or the member variable named level. I suggest renaming the latter to maximum_level.
  • The destructor is empty, you can omit its definition entirely.
  • Catching errors in the constructor: error_handler is 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 message and then to ignore the error.
  • There is no need to delete the copy constructor and assignment operator explicitly; since the member variable mutex is non-copyable, the whole class will be non-copyable by default as well.
  • Using a std::unique_ptr to hold the std::ofstream: you can default-construct a std::ofstream that is not associated with any file, and you can std::move() and std::swap() std::ofstreams just like you can with std::mutex.

Simplify the code

There is quite some complexity in log() 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 of std::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 added std::source_location, which gives you the filename and line number in a way that avoids having to use macros.

answered Aug 10, 2023 at 14:31
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.