扩展 std::cout

Extending std::cout

我想扩展 std::cout 的用法以使用我自己的 console/cout 包装器 class。

理想情况下我会有 2 个 ostream,一个用于常规打印,一个用于附加新行。

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(一个实际的新行,不仅仅是它转义)

然后我想将其扩展为具有错误流(例如 ErrorErrorLine),它们在消息前加上前缀 "ERROR: " 并以不同的颜色打印。

我知道我必须创建自己的流来添加此功能,并且我遵循 C++ cout with prefix 作为前缀 std::cout 这几乎是我想要的,但不完全是。我不知道如何在流的末尾添加新行,而且前缀不可靠,尤其是当我要执行多个打印语句时。

我还应该提到我不想使用重载运算符来实现这种效果,因为我希望能够以菊花链方式连接。

什么没用

如果我 WriteLine >> "First"; 然后 WriteLine << "Second"; 我会得到奇怪的结果,如 SecondFirst\nSecond\nFirst。我理想的输出是 First\nSecond\n。我认为这是由于 closing/flushing/resetting 流不正确,但我没有尝试让它可靠地工作。

我可以让它为单个语句工作,但是一旦我添加了另一个打印语句,我尝试打印的内容就会切换顺序,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('[=13=]'); }
    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('[=13=]'); }
    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...

感谢任何帮助and/or建议。

这里有多个问题需要不同的方法。一些描述也似乎不是很清楚实际的愿望。最有问题的要求是需要在语句末尾显然插入一个换行符。这当然是可行的,但实际上确实需要临时存在。

在去那里之前,我想指出大多数其他提供 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 现在需要是一个对象,它以某种方式导致另一个对象在使用时跳入存在,以便后者可以在析构函数中完成它的工作。使用转换将不起作用。但是,将输出运算符重载到 return 合适的对象确实有效,例如:

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.
  3. 的辅助 class

立即通过字符是相当简单的。唯一有点复杂的是知道什么时候写前缀:在第一个非换行符、非回车符-return return 之后换行符或回车符 return (其他定义可能是并且应该很容易适应)。重要的方面是流缓冲区并没有真正缓冲,而是实际上通过字符传递到底层[共享]流缓冲区:

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++ 中选择一行是否以换行符结尾的方法是编写一个换行符,该换行符应该通过合适的操纵器可能出现。