Расширение std::cout

Я хочу расширить использование std::cout использовать мой собственный класс оболочки консоли / Cout.

В идеале у меня было бы 2 ostreams, один для обычной печати и один, который добавляет новую строку.

std::ostream Write;
Write << "Hello, I am " << 99 << " years old.";

печать Hello, I am 99 years old.

std::ostream WriteLine;
WriteLine << "Hello, I am " << 99 << " years old.";

печать Hello, I am 99 years old.\n (актуальная новая строка, а не только она сбежала)

Затем я хотел бы расширить это, чтобы иметь потоки ошибок (Error а также ErrorLineнапример) какой префикс "ERROR: " перед сообщением и печатает другим цветом.

Я знаю, что должен создать свои собственные потоки, чтобы добавить эту функциональность, и я следовал Cout cout с префиксом для префикса std::cout что было почти то, что я хотел, но не совсем. Я не мог понять, как добавить новую строку в конец потока, и префикс был ненадежным, особенно когда я делал больше, чем один оператор печати.

Я должен также упомянуть, что я не хочу использовать перегруженные операторы для достижения этого эффекта, потому что я хочу иметь возможность последовательно подключаться.

Что не сработало

Если бы я сделал WriteLine >> "First"; затем WriteLine << "Second"; Я бы получил странные результаты, такие как SecondFirst\n или же Second\nFirst, Мой идеальный выход был бы First\nSecond\n, Я думаю, что это происходит из-за того, что поток не закрывался / не очищался / не сбрасывался должным образом, но ничто из того, что я пробовал, не обеспечивало его надежную работу.

Я мог заставить его работать для одного оператора, но как только я добавил другой оператор печати, вещи, которые я пытался напечатать, изменили бы порядок, исправление post / pre не было бы добавлено в правильном месте, или я бы закончил с мусором.

Меня не волнует wchars, потому что нам всегда нужен только один байт для одного символа. Также мы будем работать только на Windows 10.

Это то, что я до сих пор:

Console.h

#include <windows.h>
#include <iostream>
#include <sstream>
#include <string>

class Console {
    using Writer = std::ostream;
    Console() {}
    static const char newline = '\n';

    class error_stream: public std::streambuf {
    public:
        error_stream(std::streambuf* s) : sbuf(s) {}
        ~error_stream() { overflow('\0'); }
    private:
        typedef std::basic_string<char_type> string;

        int_type overflow(int_type c) {

            if(traits_type::eq_int_type(traits_type::eof(), c))
                return traits_type::not_eof(c);
            switch(c) {
            case '\n':
            case '\r':
            {
                SetColor(ConsoleColor::Red);
                prefix = "ERROR: ";
                buffer += c;
                if(buffer.size() > 1)
                    sbuf->sputn(prefix.c_str(), prefix.size());
                int_type rc = sbuf->sputn(buffer.c_str(), buffer.size());
                buffer.clear();
                SetColor(ConsoleColor::White);

                return rc;
            }
            default:
                buffer += c;
                return c;
            }
        }

        std::string prefix;
        std::streambuf* sbuf;
        string buffer;
    };


    class write_line_stream: public std::streambuf {
    public:
        write_line_stream(std::streambuf* s) : sbuf(s) {}
        ~write_line_stream() { overflow('\0'); }
    private:
        typedef std::basic_string<char_type> string;

        int_type overflow(int_type c) {

            if(traits_type::eq_int_type(traits_type::eof(), c))
                return traits_type::not_eof(c);
            switch(c) {
            case '\n':
            case '\r':
            {
                buffer += c;
                int_type rc = sbuf->sputn(buffer.c_str(), buffer.size());
                sbuf->sputn(&newline, 1);
                buffer.clear();

                return rc;
            }
            default:
                buffer += c;
                return c;
            }
        }

        std::streambuf* sbuf;
        string buffer;
    };

    static output_stream outputStream;
    static error_stream errorStream;
    static write_line_stream writeLineStream;

    public:
    static void Setup();

    static Writer Write;
    static Writer WriteLine;

    static Writer Err;
};

Console.cpp

#include "Console.h"

Console::Writer Console::Write(nullptr);
Console::Writer Console::WriteLine(nullptr);

Console::Writer Console::Err(nullptr);

Console::error_stream Console::errorStream(std::cout.rdbuf());
Console::write_line_stream Console::writeLineStream(std::cout.rdbuf());

void Console::Setup() {
    Write.rdbuf(std::cout.rdbuf());
    Err.rdbuf(&errorStream);
    WriteLine.rdbuf(&writeLineStream);
}

main.cpp

int main() {
    Console::Setup();
    Console::Write << "First" << "Second";
    Console::WriteLine << "Third";
    Console::WriteLine << "Fourth";
    Console::Write << "Fifth";
    Console::Error  << "Sixth";
    Console::ErrorLine  << "Seventh";
    Console::WriteLine << "Eighth";
}

Который должен дать вывод

FirstSecondThird
Fourth
FifthERROR: SixthERROR: Seventh
Eighth
Press any key to continue...

Любая помощь и / или предложения приветствуются.

1 ответ

Решение

Здесь есть несколько проблем, которые требуют разных подходов. Некоторым из описания также кажется, что фактическое желание не совсем ясно. Наиболее проблематичным требованием является то, что символ новой строки должен быть вставлен, очевидно, в конце оператора. Это, конечно, выполнимо, но эффективно требует временного присутствия.

Прежде чем перейти туда, я хочу отметить, что большинство других языков, предлагающих print-line(....) конструировать, чтобы определить, что происходит на линии с помощью вызова функции. Нет сомнений, куда идет новая строка. Если бы C++ I/O был бы создан сейчас, я был бы совершенно уверен, что он будет основан на переменной (не vararg) шаблон функции. Таким образом, напечатать что-то в конце выражения тривиально. Использование подходящего манипулятора в конце строки (хотя, вероятно, нет std::endl но может быть обычай nl) будет легкий подход.

Основой добавления новой строки в конце выражения будет использование деструктора подходящего временного объекта для его добавления. Прямой путь будет примерно таким:

#include <iostream>

class newline_writer
    : public std::ostream {
    bool need_newline = true;
public:
    newline_writer(std::streambuf* sbuf)
        : std::ios(sbuf), std::ostream(sbuf) {
    }
    newline_writer(newline_writer&& other)
        : newline_writer(other.rdbuf()) {
        other.need_newline = false;
    }
    ~newline_writer() { this->need_newline && *this << '\n'; }
};

newline_writer writeline() {
    return newline_writer(std::cout.rdbuf());
}

int main() {
    writeline() << "hello, " << "world";
}

Это работает довольно хорошо. Тем не менее, запись в вопросе не использует вызов функции. Итак, вместо того, чтобы писать

writeline() << "hello";

кажется необходимым написать

writeline << "hello";

вместо этого и до сих пор добавьте новую строку. Это немного усложняет дело: по сути, writeline теперь должен быть объектом, который каким-то образом заставляет другой объект вступать в существование при использовании, чтобы последний мог выполнять свою работу в деструкторе. Использование конверсии не сработает. Однако перегрузка оператора вывода для возврата подходящего объекта работает, например:

class writeliner {
    std::streambuf* sbuf;
public:
    writeliner(std::streambuf* sbuf): sbuf(sbuf) {}
    template <typename T>
    newline_writer operator<< (T&& value) {
        newline_writer rc(sbuf);
        rc << std::forward<T>(value);
        return rc;
    }
    newline_writer operator<< (std::ostream& (*manip)(std::ostream&)) {
        newline_writer rc(sbuf);
        rc << manip;
        return rc;
    }
} writeline(std::cout.rdbuf());

int main() {
    writeline << "hello" << "world";
    writeline << std::endl;
}

Основной целью перегруженных операторов сдвига является создание подходящего временного объекта. Они не пытаются связываться с содержимым потока персонажей. Лично я предпочел бы иметь лишние скобки, чем использовать этот несколько грязный подход, но он работает. Важным является то, что оператор также перегружен для манипуляторов, например, чтобы разрешить второе утверждение с std::endl, Без перегрузки тип endl нельзя вывести.

Следующий бит - это запись префикса и смешивание нескольких потоков. Здесь важно понять, что вы хотите одну из двух вещей:

  1. Сразу же пишите символы в общий буфер. Этот буфер больше всего похож на другой буфер потока, например, на место назначения std::streambuf,
  2. Если символ должен буферизоваться локально в отдельных потоковых буферах, соответствующие потоки должны быть сброшены своевременно, например, после каждой вставки (путем установки std::ios_base::unitbuf бит) или, самое позднее, в конце выражения, например, используя вспомогательный класс, аналогичный newline_writer,

Проходить через персонажей сразу довольно просто. Единственное небольшое затруднение - это знать, когда писать префикс: после первой не новой строки возврат без возврата каретки после перевода новой строки или возврата каретки (другие определения возможны и должны быть легко адаптируемыми). Важным аспектом является то, что потоковый буфер на самом деле не буферизуется, а фактически проходит через символ в базовый [общий] буфер потока:

class prefixbuf
    : public std::streambuf {
    std::string     prefix;
    bool            need_prefix = true;
    std::streambuf* sbuf;
    int overflow(int c) {
        if (c == std::char_traits<char>::eof()) {
            return std::char_traits<char>::not_eof(c);
        }
        switch (c) {
        case '\n':
        case '\r':
            need_prefix = true;
            break;
        default:
            if (need_prefix) {
                this->sbuf->sputn(this->prefix.c_str(), this->prefix.size());
                need_prefix = false;
            }
        }
        return this->sbuf->sputc(c);
    }
    int sync() {
        return this->sbuf->pubsync();
    }
public:
    prefixbuf(std::string prefix, std::streambuf* sbuf)
        : prefix(std::move(prefix)), sbuf(sbuf) {
    }
};

Оставшееся дело - настроить соответствующие объекты в Console Пространство имен. Однако сделать это довольно просто:

namespace Console {
    prefixbuf    errorPrefix("ERROR", std::cout.rdbuf());
    std::ostream Write(std::cout.rdbuf());
    writeliner   WriteLine(std::cout.rdbuf());
    std::ostream Error(&errorPrefix);
    writeliner   ErrorLine(&errorPrefix);
}

Я за исключением того, что подход с добавлением новой строки создает пользовательский тип, который, как мне кажется, соответствует оригинальному. Я не думаю, что временного объекта можно избежать, чтобы автоматически создавать новую строку в конце оператора.

Все это говорит о том, что вы должны использовать идиомы C++, а не пытаться копировать какой-либо другой язык в C++. Способ выбора того, заканчивается ли строка новой строкой или нет в C++, состоит в том, чтобы написать, ну, в конце концов, новую строку, где вы должны появиться потенциально посредством подходящего манипулятора.

Другие вопросы по тегам