8
\$\begingroup\$

I recently wrote this C++ Logger API code for a game engine. It needs to be as fast as possible so I tried to postpone everything to compile-time using different template meta-programming tricks (for instance, static_if). Any advice, bug fixes or suggestions are welcome.

Log.h:

// Copyright(c) Matyas Yves Constans 2016-2017
#ifndef CIANADE_LOG_H
#define CIANADE_LOG_H

#include <cstdio>
#include <ctime>
#include <sstream>

namespace cnd
{

    /** @brief Specifies the minimum log level.
     * Enumeration specifying the minimum
     * log level at which LogEntry will
     * write a message to a stream.
     * @see LogEntry
     */
    enum class LogLevel
    {
        FATAL, /// < Indicated that the application cannot go on (lowest log level)
        ERROR, /// < Indicates that an error has occured
        WARN, /// < Indicates that the application has issued a warning
        INFO, /// < Indicates that the corresponding message gives some kind of information to the end-user
        DEBUG, /// < Indicates that the corresponding message is targeted to developpers / modders / etc...

        DEBUG_1, /// < Indicates that this message must be differentiated from different debug levels
        DEBUG_2, /// < Indicates that this message must be differentiated from different debug levels
        DEBUG_3, /// < Indicates that this message must be differentiated from different debug levels
        DEBUG_4, /// < Indicates that this message must be differentiated from different debug levels (highest log level)
    };

    /** @brief Represents a Log entry, that can be modified and written to different streams.
     * An entry will be written to a specific stream when
     * an instance is destroyed, if the entry's logging level is higher
     * the reporing level. The default reporting level is LogLevel::DEBUG
     * Note that you should use one of the log printing
     * functions/macros instead to print messages.
     * @tparam T specifies which io stream to use.
     * @see LogLevel
     */
    template <typename T>
    class LogEntry
    {
    public:
        /** @brief Constructor.
         */
        LogEntry();

        /** @brief Prints current message.
         * Prints a formatted message based on the internal stream's content.
         */
        virtual ~LogEntry();

        /** @brief Returns a reference to the instance's stream.
         * Returns a reference to the stream so a message can be printed using the
         * standard c++ stream syntax.
         * Shall be overridden when inheriting from LogEntry
         * @tparam Level Specifies the log level corresponding to the message
         * @return The stream corresponding to the log entry
         */
        template <LogLevel level>
        std::ostringstream& GetMessageStream();

        /** @brief Returns the current logging level.
         * @return The Current logging level (The default log level is LogLevel::DEBUG).
         * @see LogLevel
         */
        constexpr static LogLevel GetReportingLevel();

    public:
        /** @brief Converts an std::string to a LogLevel
         * @param level An std::string representing a LogLevel. it can be either "FATAL",
         * "ERROR", "WARN", "INFO", "DEBUG", "DEBUG_1", "DEBUG_2", "DEBUG_3" or
         * "DEBUG_4".
         * @return The corresponding log level.
         */
        static LogLevel StringToLogLevel(const std::string& level); // runtime

        /** @brief Converts a LogLevel to an std::string.
         *
         * The return values can be either "ERROR", "WARN", "INFO", "DEBUG", "DEBUG_1", "DEBUG_2", "DEBUG_3"
         * or "DEBUG_4".
         * @tparam Level LogLevel that needs to be converted
         * @return The Converted std::string
         */
        template <LogLevel level>
        constexpr static std::string LogLevelToString(); // compile time

        template <LogLevel level>
        constexpr static unsigned int LogLevelToEnumIndex(); // compile time
    public:

        /** @brief Instances are non assignable! */
        LogEntry& operator=(const LogEntry& obj) = delete;
        /** @brief Instances are non assignable! */
        LogEntry(const LogEntry& obj) = delete;

    protected:
        /** @brief String stream used to output the final message.
         * Should be used to output a custom message when inheriting
         * from LogEntry.
         */
        std::ostringstream outputSStream_;

    private:
    };

    //TODO add documentation!
    class LogStream_Generic
    {
    public:
        static const FILE* GetStream();
        static void SetStream(FILE* stream);
        static void Output(const std::string& msg);
    private:
        static FILE* stream_;
    };

    template <LogLevel level = LogLevel::DEBUG>
    void WriteLog_Generic(const std::string& msg);

    template <LogLevel level = LogLevel::DEBUG>
    void WriteLog(const std::string& msg);

    #define CIANADE_LOG_STREAM(logLevel)\
        if (cnd::LogEntry<cnd::LogStream_Generic>::LogLevelToEnumIndex<logLevel>() > /* (probably) compile time */\
        cnd::LogEntry<cnd::LogStream_Generic>::LogLevelToEnumIndex<cnd::LogEntry<cnd::LogStream_Generic>::GetReportingLevel()>());\
        else cnd::LogEntry<cnd::LogStream_Generic>().GetMessageStream<logLevel>()
}

#include "Cianade/System/Details/Log_details.h"
#endif //CIANADE_LOG_H

Log_details.h: (implementation)

// Copyright(c) Matyas Yves Constans 2016-2017
// @file Log_details.h - 12/23/2016

#include <string>
#include <cstdio>
#include "../Time.h"
#include "../StaticIf.h"

template <typename T>
cnd::LogEntry<T>::LogEntry() = default;

template <typename T>
cnd::LogEntry<T>::~LogEntry()
{
    outputSStream_ << "\n" << std::flush;
    T().Output(outputSStream_.str());
}

template <typename T>
template <cnd::LogLevel level>
constexpr std::string cnd::LogEntry<T>::LogLevelToString()
{
    return level == LogLevel::FATAL ? "FATAL" : // compile time
            ( level == LogLevel::ERROR ? "ERROR" :
            ( level == LogLevel::WARN ? "WARN" : 
            ( level == LogLevel::INFO ? "INFO" : 
            ( level == LogLevel::DEBUG ? "DEBUG" : 
            ( level == LogLevel::DEBUG_1 ? "DEBUG_1" : 
            ( level == LogLevel::DEBUG_2 ? "DEBUG_2" : 
            ( level == LogLevel::DEBUG_3 ? "DEBUG_3" : 
            ( level == LogLevel::DEBUG_4 ? "DEBUG_4" : 
            "" /* Should never happen, unless enum is extended*/
            ))))))));
}

template <typename T>
template <cnd::LogLevel level>
constexpr unsigned int cnd::LogEntry<T>::LogLevelToEnumIndex()
{
    return static_cast<std::underlying_type_t<cnd::LogLevel>>(level);
}

template <typename T>
cnd::LogLevel cnd::LogEntry<T>::StringToLogLevel(const std::string& level)
{
    switch (level)
    {
        case "INFO":
            return LogLevel::INFO;
        case "WARN":
            return LogLevel::WARN;
        case "DEBUG":
            return LogLevel::DEBUG;
        case "ERROR":
            return LogLevel::ERROR;
        case "FATAL":
            return LogLevel::FATAL;
        case "DEBUG_1":
            return LogLevel::DEBUG_1;
        case "DEBUG_2":
            return LogLevel::DEBUG_2;
        case "DEBUG_3":
            return LogLevel::DEBUG_3;
        case "DEBUG_4":
            return LogLevel::DEBUG_4;
        default:
            return "";
    }
}

template <typename T>
template <cnd::LogLevel level>
std::ostringstream& cnd::LogEntry<T>::GetMessageStream()
{
    outputSStream_ << "- " << time::GetCurrentTimestamp();
    outputSStream_ << " " << LogLevelToString<level>() << ": ";
    return outputSStream_;
}

template <typename T>
constexpr cnd::LogLevel cnd::LogEntry<T>::GetReportingLevel()
{
#ifndef CIANADE_MAX_LOGLEVEL
    return LogLevel::DEBUG;
#else
    return CIANADE_MAX_LOGLEVEL;
#endif
}


FILE * cnd::LogStream_Generic::stream_ = stdout;

const FILE* cnd::LogStream_Generic::GetStream()
{
    return stream_;
}

void cnd::LogStream_Generic::SetStream(FILE* stream)
{
    stream_ = stream;
}

void cnd::LogStream_Generic::Output(const std::string &msg)
{
    if (!stream_)
        return;
    fprintf(stream_, "%s", msg.c_str());
    fflush(stream_);
}

template <cnd::LogLevel L>
void cnd::WriteLog_Generic(const std::string& msg)
{
    constexpr unsigned int current_level 
            = LogEntry<LogStream_Generic>::LogLevelToEnumIndex<L>();
    constexpr unsigned int max_level 
            = LogEntry<LogStream_Generic>::LogLevelToEnumIndex<LogEntry<LogStream_Generic>::GetReportingLevel()>();
    constexpr bool condition = current_level <= max_level;

    staticIf<condition>([&](auto f){ // compile time
        LogEntry<LogStream_Generic>().GetMessageStream<L>() << msg;
    });
}

template <cnd::LogLevel level>
void cnd::WriteLog(const std::string& msg) { WriteLog_Generic<level>(msg); }

main.cpp (example)

#define CIANADE_MAX_LOGLEVEL cnd::LogLevel::DEBUG
#include <Cianade/System/Log.h>

int main(int argc, char **argv)
{
    cnd::WriteLog<cnd::LogLevel::DEBUG>("Hello -.-"); // puts syntax
    CIANADE_LOG_STREAM(cnd::LogLevel::INFO) << "Hello ^-^"; // stream syntax

    cnd::WriteLog<cnd::LogLevel::DEBUG_2>("nope"); // not displayed
    return 0;
}
\$\endgroup\$
3
+50
\$\begingroup\$

You missed the most obvious issue.

cnd::WriteLog<cnd::LogLevel::DEBUG>(getMessage("BLa", "Bla", 5));

If the log level DEBUG is not going to print anything. Then calling the function getMessage() is a complete waste of time. You need to defer the call of getMessage() and only call it when it will generate something useful.

Bad Code:

This is not even legal C++

cnd::LogLevel cnd::LogEntry<T>::StringToLogLevel(const std::string& level)
{
    switch (level)  // level must be an integer type
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ thanks, I didn't even notice that it was not valid code! It's weird that gcc/msvc didn't even complain or issue a warning. Also can you give me an example about deferring the call to "getMessage()" ? \$\endgroup\$ – MattMatt Dec 27 '16 at 23:21
  • 1
    \$\begingroup\$ @MattMatt templates are not actually compiled until they are instantiated. So this just means you don't use that part of the code. \$\endgroup\$ – Martin York Dec 27 '16 at 23:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.