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