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 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);}#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

askedAug 10, 2023 at 11:01
Marc's user avatar
\$\endgroup\$

1 Answer1

3
\$\begingroup\$

Unnecessary things

  • You don't need to forward declareclass Snoop, nothing uses it before you actually define the class.
  • this-> is almost never necessary in C++. The only issue is withlevel inSnoop::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_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 messageand then to ignore the error.
  • There is no need to delete the copy constructor and assignment operator explicitly; since the member variablemutex is non-copyable, the whole class will be non-copyable by default as well.
  • Using astd::unique_ptr to hold thestd::ofstream: you can default-construct astd::ofstream that 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.

answeredAug 10, 2023 at 14:31
G. Sliepen's user avatar
\$\endgroup\$

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.