読者です 読者をやめる 読者になる 読者になる

C++ でロガーっぽい何か

C++

わんくまで見た方法を使ってでっちあげてみた。
C++ のコードを書いたのは久しぶりな気がする・・・


オリジナルと違う部分は、純粋仮想関数で固めた log_adapter でどうのこうのするんじゃなくて、template 使ったところ。
どーせこの辺は静的に決まるだろうから、わざわざ動的なディスパッチは必要ないんじゃないかなぁ。


ただ、log_info の with_logger で log_info_with_logger 生成して返すとか、なんだか微妙。
実際にログ出力を行うクラス側に operator() を持たせるとかしてごにょごにょやればいいんだろうけど・・・
そうすると実際にログ出力を行う側の記述が面倒になっちゃうんだよなぁ・・・

以下コード。

#include <iostream>
#include <cstdarg>
#include <cstdio>

// グローバルを汚したくないのでstructでラップ
struct log_level
{
    enum type
    {
        DEBUG,
        INFO,
        WARN,
        ERROR,
        FATAL
    };
};

std::ostream& operator<<(std::ostream& os, log_level::type level)
{
    switch (level) {
    case log_level::DEBUG: os << "DEBUG"; break;
    case log_level::INFO: os << "INFO"; break;
    case log_level::WARN: os << "WARN"; break;
    case log_level::ERROR: os << "ERROR"; break;
    case log_level::FATAL: os << "FATAL"; break;
    }
    return os;
}

// 実際にログ出力を行うクラスに情報を渡す、薄いラッパー
template <class Logger>
class log_info_with_logger
{
    const Logger& logger;
    const char* file_name;
    const int line_num;
    const log_level::type level;
public:
    log_info_with_logger(
        const Logger& logger,
        const char* file_name,
        int line_num,
        log_level::type level)
        : logger(logger), file_name(file_name), line_num(line_num), level(level)
    {}

    void operator()(const char* format, ...) const
    {
        std::va_list args;
        va_start(args, format);
        logger.put(file_name, line_num, level, format, args);
        va_end(args);
    }
};

// ファイル名や行番号などを保持するクラス
class log_info
{
    const char* file_name;
    const int line_num;
    const log_level::type level;
public:
    log_info(const char* file_name, int line_num, log_level::type level)
        : file_name(file_name), line_num(line_num), level(level)
    {}

    template <class Logger>
    log_info_with_logger<Logger> with_logger(const Logger& logger) const
    {
        return log_info_with_logger<Logger>(logger, file_name, line_num, level);
    }
};

// 実際にログを埋め込む場所で使用するマクロ
// loggerという名前で実際にログ出力するためのオブジェクトにアクセス可能である必要がある
// あとlog_level::typeの各列挙子は外部からは使わない前提
#define DEBUG log_info(__FILE__, __LINE__, log_level::DEBUG).with_logger(logger)
#define INFO log_info(__FILE__, __LINE__, log_level::INFO).with_logger(logger)
#define WARN log_info(__FILE__, __LINE__, log_level::WARN).with_logger(logger)
#define ERROR log_info(__FILE__, __LINE__, log_level::ERROR).with_logger(logger)
#define FATAL log_info(__FILE__, __LINE__, log_level::FATAL).with_logger(logger)

// 実際にログ出力を行うクラス
struct stdout_logger
{
    void put(const char* file_name, int line_num, log_level::type level, const char* format, std::va_list args) const
    {
        std::cout << level << " "
                  << file_name << "(" << line_num << "): ";
        std::vprintf(format, args);
        std::cout << std::endl;
    }
};

// 使用例
int main()
{
    stdout_logger logger;

    DEBUG("hoge");
    ERROR("piyo %d", 10);
}

あとはファイルに書き込む Logger とか、DB に書き込む Logger を作って、stdout_logger のように使うだけ。


途中で生成するオブジェクトを減らしたバージョンはこんな感じ。

#include <iostream>
#include <cstdarg>
#include <cstdio>

// ここら辺は同じ
struct log_level
{
    enum type
    {
        DEBUG,
        INFO,
        WARN,
        ERROR,
        FATAL
    };
};

std::ostream& operator<<(std::ostream& os, log_level::type level)
{
    switch (level) {
    case log_level::DEBUG: os << "DEBUG"; break;
    case log_level::INFO: os << "INFO"; break;
    case log_level::WARN: os << "WARN"; break;
    case log_level::ERROR: os << "ERROR"; break;
    case log_level::FATAL: os << "FATAL"; break;
    }
    return os;
}

class log_info;

// 実際にログ出力を行うクラス側の負担を減らすための基底クラス
class logger_base
{
protected:
    const char* file_name;
    int line_num;
    log_level::type level;
public:
    void set_log_info(const log_info& info);
};

// log_info_with_loggerが消えて、with_loggerがset_info_toになった感じ
class log_info
{
    // logger_baseはお友達
    friend class logger_base;

    const char* file_name;
    const int line_num;
    const log_level::type level;
public:
    log_info(const char* file_name, int line_num, log_level::type level)
        : file_name(file_name), line_num(line_num), level(level)
    {}

    // 上のバージョンのwith_loggerと違って、loggerに情報をセットしてloggerを返す
    template <class Logger>
    Logger& set_info_to(Logger& logger) const
    {
        logger.set_log_info(*this);
        return logger;
    }
};

void logger_base::set_log_info(const log_info& info)
{
    file_name = info.file_name;
    line_num = info.line_num;
    level = info.level;
}

// ここら辺のマクロはほとんど同じ
#define DEBUG log_info(__FILE__, __LINE__, log_level::DEBUG).set_info_to(logger)
#define INFO log_info(__FILE__, __LINE__, log_level::INFO).set_info_to(logger)
#define WARN log_info(__FILE__, __LINE__, log_level::WARN).set_info_to(logger)
#define ERROR log_info(__FILE__, __LINE__, log_level::ERROR).set_info_to(logger)
#define FATAL log_info(__FILE__, __LINE__, log_level::FATAL).set_info_to(logger)

// logger_baseクラスから派生
struct stdout_logger : private logger_base
{
    using logger_base::set_log_info;

    // 上のバージョンと違って、こっちでoperator()をオーバーロードする
    void operator()(const char* format, ...) const
    {
        std::cout << level << " "
                  << file_name << "(" << line_num << "): ";

        std::va_list args;
        va_start(args, format);
        std::vprintf(format, args);
        va_end(args);

        std::cout << std::endl;
    }
};

// 使用例
int main()
{
    stdout_logger logger;

    DEBUG("hoge");
    ERROR("piyo %d", 10);
}

んー、実際にログ出力を行うクラスは一度作ってしまえば使い回しが出来るから、オブジェクトの生成回数を減らせるこっちの方がいい・・・のかな・・・?