Варианты operator<< для логгера

в 10:08, , рубрики: c++, logging c++, Программирование

Введение

Уважаемыее, у меня родилась публикация — вопрос. С удовольствием выслушаю вашу критику в комментариях.

В проекте пришлось набросать небольшую обертку над существующей системой логирования и помимо ее основного предназначения захотелось ее использовать в стиле работы с потоком вывода.

Опуская не нужные детали обертка выглядит так:

class LoggerWrap: public RequiredInterface
{
public:
    void write(const std::string& str) const; //запись в существующую систему логирования
/*
 ... прочие методы и поля класса реализующие RequiredInterface ...
 */
};

А дополнительное желаемое использование так:

LoggerWrap log;
log << "значение 1: " << std::setw(10) << someValue << "; значение 2:" << std::setw(15) << anotherValue;

В процессе размышлений мне пришло в голову несколько решений которые я и хочу обсудить.

Вариант 1

Перегрузить operator<<, данные полученные им направлять в локальный для потока ostringstream. В конце писать в лог специальный объект — признак конца строки.

Решение может выглядеть примерно так:

class LoggerWrap
{
public:
    void write(const std::string& str) const
    {
        //просто для примера
        std::cout << "[Log]: " << str << std::endl;
    }
/*
 ... прочие методы и поля класса ...
 */
    struct Flush {}; //признака конца строки

    template<typename T>
    LoggerWrap& operator<< (const T& data)
    {
        buf << data;
        return *this;
    }

    LoggerWrap& operator<< (const Flush&)
    {
        write(buf.str());
        buf.str("");
        buf.flags(defFmtFlags);
        return *this;
    }

private:
    thread_local static std::ostringstream buf;
    const thread_local static std::ios::fmtflags defFmtFlags;
};

thread_local std::ostringstream LoggerWrap::buf;
const thread_local std::ios::fmtflags LoggerWrap::defFmtFlags(buf.flags());

Использование:

LoggerWrap logger;
    logger << "#" << 1 << ": " << 1.2 << ", text again" << LoggerWrap::Flush();
    logger << "#" << 2 << ": " << std::scientific << 2.3 << ", text again" << LoggerWrap::Flush();
    logger << "string #" << 3 << ": " << 10.5 << LoggerWrap::Flush();

в консоле будет напечатано:

[Log]: #1: 1.2, text again
[Log]: #2: 2.300000e+00, text again
[Log]: #3: 10.5

Для меня, недостатком этого варианта является необходимость писать LoggerWrap::Flush(). Его можно забыть написать и потом долго пытаться понять, а что за чертовщина происходит в логе.

Вариант 2

Как определить, что строка для логирования завершена, без явного указания на это? Я решил опереться на время жизни временного объекта. Когда функция возвращает объект, то он живет пока на него есть ссылка. Таким образом можно создать временный объект со своим operator<<, возвращающим ссылку на этот объект, а его деструктор будет вызывать метод LoggerWrap::write.

Получается следующее:

class LoggerWrap
{
public:
    void write(const std::string& str) const
    {
        //просто для примера
        std::cout << "[Log]: " << str << std::endl;
    }
/*
 ... прочие методы и поля класса ...
 */

    class TmpLog
    {
        friend class LoggerWrap;
    public:
        ~TmpLog()
        {
            if (flush)
            {
                logger.write(buf.str());
                buf.str("");
            }
        }

        template<typename T>
        TmpLog& operator<< (const T& data)
        {
            buf << data;
            return *this;
        }

        TmpLog(const TmpLog&) = delete;
    private:
        TmpLog(const LoggerWrap& logger, std::ostringstream& buf) :
            logger(logger),
            buf(buf)
        {
        }

        TmpLog(TmpLog&& that):
            logger(that.logger),
            buf(that.buf),
            flush(that.flush)
        {
            that.flush = false;
        }

        const LoggerWrap& logger;
        std::ostringstream& buf;
        bool flush = true;
    };

    template<typename T>
    TmpLog operator<< (const T& data)
    {
        buf.flags(defFmtFlags);
        TmpLog tmlLog(*this, buf);
        return std::move(tmlLog << data);
    }
private:
    thread_local static std::ostringstream buf;
    const thread_local static std::ios::fmtflags defFmtFlags;
};

thread_local std::ostringstream LoggerWrap::buf;
const thread_local std::ios::fmtflags LoggerWrap::defFmtFlags(buf.flags());

и использование:

    LoggerWrap logger;

    logger << "#" << 1 << ": " << 1.2 << ", text again";
    logger << "#" << 2 << ": " << std::scientific << 2.3 << ", text again";
    logger << "#" << 3 << ": " << 10.5;

вывод в консоле будет аналогичен первому решению.

Итоги

Здесь уровня логирования (Warning, Error, Info и т.п.) так как наша система логирования их не различает, но добавить это в обертку не сложно. Например определить LoggerWrap::operator(), принимающий в качестве аргумента желаемый уровень для строки, которая будет выведена.

Мне больше нравится второе решение, так как не нужно дописывать после каждой строки некоторое магическое слово. А при добавлении уровней логирования, во временном объекте можно будет хранить эту информацию для текущей строки.

Спасибо, что дочитали, надеюсь увидеть ваше мнение в комментариях.

Автор: Hokum

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js