Search code examples
c++templatesvariadic-templatesbuilderpolicy

Policy based design combined with builder


I'm designing a logger library in C++, and I've been stuck with my implementation of a formatter class.

My goal is to be able to create a message "formatter", which will take a string (something like "[A] [B]", or "[B]") and decide which policies to attach to my formatter.

Here is a simplified version of my policy implementation:

#include <iostream>
#include <string>
#include <memory>

template <typename PolicyA, typename PolicyB>
class Formatter : private PolicyA, private PolicyB {
    public:
    void Format(std::string& param_message) const {
        PolicyA::A(param_message);
        PolicyB::B(param_message);
    }
};

class Default {
protected:
    virtual void A(std::string& param_message) const = 0;
};

class ADefault : public virtual Default {
protected:
    void A(std::string& param_message) const {}
};

class AExplicit : public virtual Default {
protected:
    void A(std::string& param_message) const { param_message = "[A] " + param_message; }
};

class BDefault : public virtual Default {
protected:
    void B(std::string& param_message) const {}
};

class BExplicit : public virtual Default {
protected:
    void B(std::string& param_message) const { param_message = "[B] " + param_message; }
};

int main() {
    std::string message_1 = "message_1";
    std::string message_2 = "message_2";

    Formatter<ADefault, BDefault> default_message;
    Formatter<AExplicit, BExplicit> explicit_message;
    
    default_message.Format(message_1);
    explicit_message.Format(message_2);
    std::cout << message_1 << std::endl;
    std::cout << message_2 << std::endl;
}

The policy works as I intended, but how would I go about creating a sort of Builder function? One that could return a Fromatter from a set of parameters. I already have an implementation of the string interpreter, which returns an enum buffer of formats:

enum class Format : uint16_t {
    NONE = 0x000, 
    A    = 0x001,
    B    = 0x002,
};


Format ConfigFormatter(std::string& param_configuration) {
    Format formatter_configuration = Format::NONE;

    if (param_configuration.find("[A]") != std::string::npos) {
        formatter_configuration += Format::A;
    }

    if (param_configuration.find("[B]") != std::string::npos) {
        formatter_configuration += Format::B;
    }

    return formatter_configuration;
}

Is it possible to store the created Formatter as a member variable of another class? And should the Builder class be a variadic class with specializations for every type? I've looked for on variadic factories, but could not find anything that could help me.


Solution

  • Maybe this could help:

    #include <iostream>
    #include <sstream>
    #include <string>
    #include <string_view>
    
    enum class LogLevel { Debug, Info, Warn, Error, Fatal, Off };
    
    template <typename... Fmts>
    struct Logger {
        template <LogLevel Level>
        inline void Log(std::string_view msg) {
            if constexpr (Level == LogLevel::Off) {
                return;
            }
    
            std::stringstream out{};
    
            ((Fmts::template Format<Level>(out)), ...);
    
            std::clog << out.str() << msg << std::endl;
        }
    };
    
    template <typename... Fmts>
    struct LoggerBuilder {
        template <typename Fmt>
        inline auto With() {
            return LoggerBuilder<Fmts..., Fmt>{};
        }
    
        inline auto Build() {
            return Logger<Fmts...>{};
        }
    };
    
    struct DefaultFormatter {
        template <LogLevel Level>
        static inline void Format(std::stringstream& buf) {
        }
    };
    
    struct ExplicitFormatter {
        template <LogLevel Level>
        static inline void Format(std::stringstream& buf) {
            using enum LogLevel;
    
            if constexpr (Level == Debug) {
                buf << "[DEBUG] ";
            } else if constexpr (Level == Info) {
                buf << "[INFO] ";
            } else if constexpr (Level == Warn) {
                buf << "[WARN] ";
            } else if constexpr (Level == Error) {
                buf << "[ERROR] ";
            } else if constexpr (Level == Fatal) {
                buf << "[FATAL] ";
            }
        }
    };
    
    struct LongTimeFormatter {
        template <LogLevel Level>
        static inline void Format(std::stringstream& buf) {
            buf << "[00:00:00.00 00/00/0000] ";
        }
    };
    
    int main() {
        auto logger = LoggerBuilder{}
                          .With<LongTimeFormatter>()
                          .With<ExplicitFormatter>()
                          .Build();
    
        logger.Log<LogLevel::Debug>("My Debug Message");
        logger.Log<LogLevel::Info>("My Info Message");
        logger.Log<LogLevel::Warn>("My Warn Message");
        logger.Log<LogLevel::Error>("My Error Message");
        logger.Log<LogLevel::Fatal>("My Fatal Message");
        logger.Log<LogLevel::Off>("Not Printed");
    
        return 0;
    }
    

    It uses templates rather than RTTI, so you get almost a zero-cost abstraction.