Search code examples
c++templatesrecursionoverloadingparameter-pack

method with template parameter pack is called recursivly indefenetly, and doesnt call overloaded method


I have a logging class, which logs messages of any type:

/// log level enum
enum level_t
{
    DEBUG,   // used for debugging output
    CPLEX,   // used for cplex output
    INFO,    // used for generell informative output
    WARNING, // used for warning
    ERROR,   // used for error
};

/// converts a level_t type to a string
std::string lvl2str(level_t level)
{
    switch (level)
    {
    case DEBUG:
        return "debug";
    case CPLEX:
        return "cplex";
    case INFO:
        return "info";
    case WARNING:
        return "warning";
    case ERROR:
        return "error";
    default:
        throw std::runtime_error("there is no such log level");
    }
}

/// logger class.
///
/// use the log method to log information.
///
/// if you want to only log some information or simply filter the log output,
/// you can provide filter function (trough the constructor) in which you return true if a message should be logged
/// and false if it should not be logged. see the Logger::filter_t type for more information.
class Logger
{
  public:
    /// alias for the filter type.
    /// this is a function, which should return true, if a message should be logged, and false otherwise.
    /// you can log for example all messages with a log-level < INFO and/or messages starting with "MLU".
    /// this filter is extremely flexible, as it hands the logic to the user.
    using filter_t = std::function<bool(level_t, const std::string &)>;

  protected:
    /// default filter, used in the constructor.
    /// logs all messages
    constexpr static auto default_filter = [](level_t, const std::string &) { return true; };

  private:
    /// filter for filtering messages before logging them
    filter_t filter;

  public:
    Logger(filter_t filter = default_filter) : filter(std::move(filter))
    {
    }

    /// getter for the filter object
    filter_t get_filter() const
    {
        return this->filter;
    }

    /// logs a message, but only if the message passes the filter
    /// \tparam T type of the logged message
    /// \param level log level of the message
    /// \param filter function, which filters undesired log messages
    /// \param msg message to be logged
    template <typename T> static void log(level_t level, filter_t filter, const T &msg)
    {
        // convert the msg of template type T to a string, so we can apply the filter on the message
        std::string m = std::format("{}", msg);

        if (filter(level, m))
        {
            // get current local time
            time_t t = std::time(nullptr);
            std::tm tm = *std::localtime(&t);
            // convert time to string
            std::ostringstream oss;
            oss << std::put_time(&tm, "%H:%M:%S");
            auto time_str = oss.str();

            // generate log message
            auto log_msg = std::format("[{}] [{}] {}", time_str, lvl2str(level), msg);

            // log the message
            std::cout << log_msg << std::endl;
        };
    }

    /// logs a message, but only if the message passes the filter.
    /// this method also supports format strings.
    /// \tparam Args argument types for the format string
    /// \param level log level of the message
    /// \param filter function, which filters undesired log messages
    /// \param fmt format string
    /// \param args arguments for the format string
    template <typename... Args> static void log(level_t level, filter_t filter, const std::string &fmt, Args &&...args)
    {
        // append all args to a **single** message string
        auto formatted_msg = std::vformat(fmt, std::make_format_args(args...)); // todo [1]

        // pass the message to the logging function
        Logger::log(level, filter, formatted_msg);
    }


    /// logs a message, but only if the message passes the filter.
    /// same as the static log method, uses the filter object passed with the compiler.
    /// \tparam T type of the logged message
    /// \param level log level of the message
    /// \param msg message to be logged
    template <typename T> void log(level_t level, const T &msg)
    {
        Logger::log(level, this->filter, msg);
    }

    /// logs a message, but only if the message passes the filter.
    /// this method also supports format strings.
    /// same as the static log method, uses the filter object passed with the compiler.
    /// \tparam Args argument types for the format string
    /// \param level log level of the message
    /// \param fmt format string
    /// \param args arguments for the format string
    template <typename... Args> void log(level_t level, const std::string &format_string, Args &&...args)
    {
        Logger::log(level, this->filter, format_string, std::forward<Args>(args)...);
    }
};

and the example code:

int main(int argc, char *argv[])
{
    // add filter for creation of the logger
    Logger::filter_t filter = [](level_t, const std::string&){ return true; };
    // create logger class with filter
    Logger logger(filter);

    logger.log(level_t::INFO, "hello world"); // <-- [1] works as expected
    logger.log(level_t::INFO, "hello [{}] {}", "world", 42); // <-- [2] does not work

    return 0;
}

The method call marked with [1] works as expected, but the method call marked with [2] does not work, instead it exits with the following exit code: Process finished with exit code 139 (interrupted by signal 11: SIGSEGV). I debugged the program and it seems, that in the method template <typename... Args> static void log(level_t level, filter_t filter, const std::string &fmt, Args &&...args) the call Logger::log(level, filter, formatted_msg); calls the same method/ itself! This is not the behavior, which I would expected. Instead I expect, the method to call the template <typename T> static void log(level_t level, filter_t filter, const T &msg) method, because I didn't provide a pack parameter.

  1. Why does the error appear even though I have a overloaded function (same name, but different parameters)?
  2. How can I fix the error?

Solution

  • All in all, a total redesign of the code seems essential. The none-variadic overload of Logger::log seems to be preparing a time prefix. I would initially recommend std::chrono for time, but everything regarding time and locality has a tone of caveats, and standard portable solutions might fail at compile or runtime - depending on platform.

    Regardless of the time formatting issues, the actual API function the OP seems to need is the none-static variadic template. The static version seems to have convenience usage too. But the none-variadic version can simply be removed from the API, in order to totally avoid the overload resolution process.

    In order to enable compile-time diagnostics for logging argument mismatch - instead of runtime errors - C++20 provides std::format_string:

    private:
    static std::ostream& log_prefix(level_t level){
        return std::cout << FORMATED_TIME << FORMAT_LEVEL(level);
        //TODO: FORMATED_TIME and FORMAT_LEVEL should be handled
    };
    
    public:
    
    template <typename... Args>
    static void log(level_t level, filter_t& filter, const std::format_string<Args ...> fmt, Args &&...args){
         auto msg = std::format(fmt, args...);
         if (!filter(level, msg))
             return;
         Logger::log_prefix(level) << msg;
    };
    
    template <typename... Args>
    void log(level_t level, const std::format_string<Args ...> fmt, Args &&...args){
        Logger::log(level, this->filter, fmt, std::forward<Args>(args)...); 
    };
    
    

    However, I would recommend the OP to choose a different approach on filtering the message; E.G. check the formatting onlyfilter(level, fmt.get()), or other light weight strategies. because formatting already allocates and calculates the printable result.