std::endl的过载处理方式?

37

我想定义一个名为MyStream的类,使得:

MyStream myStream;
myStream << 1 << 2 << 3 << std::endl << 5 << 6 << std::endl << 7 << 8 << std::endl;

输出结果

[blah]123
[blah]56
[blah]78

我的目标是在每个非终止的std::endl后面插入一个"[blah]",并将其插入到开头。难点不在于逻辑管理,而在于检测和重载对std::endl的处理。有没有一种优雅的方法来实现这个目标?

谢谢!

编辑:我不需要逻辑管理方面的建议。我需要知道如何检测/重载打印std::endl


你只需要覆盖流缓冲区,使用具有自己独特版本的sync()函数即可。 - Martin York
7个回答

35
你需要做的是编写自己的流缓冲区:当流缓冲区被刷新时,输出前缀字符和流内容。以下代码有效是因为std::endl引起了以下操作: 1.将'\n'添加到流中。 2.在流上调用flush()方法。 3.这会在流缓冲区上调用pubsync()方法。 4.该方法调用虚函数sync()。 5.覆盖此虚函数以执行所需的操作。
#include <iostream>
#include <sstream>

class MyStream: public std::ostream
{
    // Write a stream buffer that prefixes each line with Plop
    class MyStreamBuf: public std::stringbuf
    {
        std::ostream&   output;
        public:
            MyStreamBuf(std::ostream& str)
                :output(str)
            {}
            ~MyStreamBuf() {
                if (pbase() != pptr()) {
                    putOutput();
                }
            }
   
        // When we sync the stream with the output. 
        // 1) Output Plop then the buffer
        // 2) Reset the buffer
        // 3) flush the actual output stream we are using.
        virtual int sync() {
            putOutput();
            return 0;
        }
        void putOutput() {
            // Called by destructor.
            // destructor can not call virtual methods.
            output << "[blah]" << str();
            str("");
            output.flush();
        }
    };

    // My Stream just uses a version of my special buffer
    MyStreamBuf buffer;
    public:
        MyStream(std::ostream& str)
            :std::ostream(&buffer)
            ,buffer(str)
        {
        }
};


int main()
{
    MyStream myStream(std::cout);
    myStream << 1 << 2 << 3 << std::endl << 5 << 6 << std::endl << 7 << 8 << std::endl;
}
> ./a.out
[blah]123 
[blah]56 
[blah]78
>

1
你抄袭了我的建议,却没有给出任何信用,反而批评我(什么?),而且只做了一半。你没有覆盖overflow,所以如果在调用sync之前putc('\n')导致溢出,你的代码将失败。 - Potatoswatter
1
如果您应用令牌编辑,我会撤销那个投票...明天。 - Potatoswatter
3
有点自负以为我抄袭了你,其实没有抄袭。你提出了一些可能性的描述,我提出了解决方案。我们完全是独立地做到的。猜猜怎么着:我之前也做过类似的事情 :-) - Martin York
从Joseuutis的C++标准库中,这个代码是有效的:std::cin.tie(&myStream); 有没有办法将其纳入到MyStream类中?我仍在尝试让程序结束时触发刷新。 - Macbeth's Enigma
顺便说一句,我把这个问题放在了SO的形式中。http://stackoverflow.com/questions/17532460/can-a-custom-stream-buffer-automatically-flush-at-program-exit-and-when-requesti - Macbeth's Enigma
显示剩余8条评论

18

你的 MyStream 类的重载运算符必须设置一个前一个打印的标记是 std::endl 的标志。

然后,如果下一个对象被打印,[blah] 就可以插入到它前面。

std::endl 是一个接受并返回 std::ostream 引用的函数。为了检测它是否被移入了你的流中,你需要在你的类型和这样一个函数之间重载 operator<<

MyStream& operator<<( std::ostream&(*f)(std::ostream&) )
{
    std::cout << f;

    if( f == std::endl )
    {
        _lastTokenWasEndl = true;
    }

    return *this;
}

3
不幸的是,这不仅检测 std::endl - 它还检测任何没有参数的操作器。但我猜在 <<() 运算符中,您可以将 f 与 std::endl 进行比较,如果它们是相同的东西,则进行特殊处理。 - anon
1
在编辑帖子的过程中,我不确定与 endl 的比较是否有效,但我进行了测试,它是有效的 :-) - Timbo
2
这个方法不可行的原因与你永远无法从ostream派生有关:格式化插入器被定义为返回一个ostream&。即使你覆盖了所有默认值,仍然有用户定义的ostream &operator<<( ostream &, my_type const & )存在。然后my_stream << my_type(5) << endl;调用的是operator<<( ostream&, manipulator ),而不是operator<<( MyStream&, manipulator ) - Potatoswatter
代码现在将关闭所有其他的操作器 - 你需要在 "else" 逻辑中调用它们:f(* this); - anon
2
这应该可以工作,除了GCC抱怨“假设将类型' std :: basic_ostream <char,std :: char_traits <char >>&>(*)(std :: basic_ostream <char,std :: char_traits <char >>&)'从重载函数转换为类型”在将fstd :: endl进行比较时。@Neil:这不会“关闭”任何操作器;它们被传递给std :: cout并在那里应用。 - Jon Purdy
1
@Gabriel,你应该按照GCC的建议去做:将if (f == (std::basic_ostream<char>& (*)(std::basic_ostream<char>&)) &std::endl)进行强制类型转换。 - j0nnyf1ve

2

我使用函数指针。对于不熟悉C语言的人来说,这听起来很可怕,但在大多数情况下它更加高效。以下是一个例子:

#include <iostream>

class Foo
{
public:
    Foo& operator<<(const char* str) { std::cout << str; return *this; }
    // If your compiler allows it, you can omit the "fun" from *fun below.  It'll make it an anonymous parameter, though...
    Foo& operator<<(std::ostream& (*fun)(std::ostream&)) { std::cout << std::endl; }
} foo;

int main(int argc,char **argv)
{
    foo << "This is a test!" << std::endl;
    return 0;
}

如果你真的想要,可以检查 endl 的地址以确认你没有得到其他的 void/void 函数,但我认为在大多数情况下这并不值得。希望这有所帮助。


2

我原则上同意Neil的意见。

您想改变缓冲区的行为,因为这是扩展iostreams的唯一方法。endl会实现此操作:

flush(__os.put(__os.widen('\n')));

widen返回单个字符,因此您不能将字符串放在其中。 put调用putc,它不是虚函数,仅偶尔钩住overflow。 您可以拦截flush,它调用缓冲区的sync。 您需要在overflow或手动sync时拦截并更改所有换行符为您的字符串。

设计一个覆盖缓冲区类很麻烦,因为basic_streambuf希望直接访问其缓冲区内存。 这会阻止您轻松地将I / O请求传递给预先存在的basic_streambuf。 您需要冒险并假设您知道流缓冲区类,并从中派生。 (就我所知,cincout不能保证使用basic_filebuf。)然后,只需添加virtual overflowsync。 (请参见§27.5.2.4.5 / 3和27.5.2.4.2 / 7。)执行替换可能需要额外的空间,因此请注意提前分配。

- 或者 -

只需在自己的命名空间中声明一个新的endl,或者更好的是,一个不叫endl的操纵器!


你正在过度复杂化事情。使用std::stringbuf类来进行缓冲。 - Martin York
@Martin:他问如何更改ostream的行为。我只能假设他想要处理文件。无论如何,即使您使用stringbuf,您仍处于派生、重载和替换的相同情况下:它并没有简化任何事情。另一方面,忘记整个流混乱,并在mystringstream.str()的结果上执行搜索和替换将是非常合理的! - Potatoswatter
@Patatoswatter:请阅读我的解决方案。我认为30行代码(包括注释)相对来说是比较简单的。 - Martin York
@Martin:是的,它很简单,但在复杂度方面,它介于我想象的和“使用std::stringbuf类”之间。 - Potatoswatter

1

与其试图修改std::endl的行为,您应该创建一个过滤流缓冲区来完成这项工作。James Kanze有一个示例展示如何在每个输出行的开头插入时间戳。只需要进行小修改即可将其更改为所需的任何前缀。


1
我有同样的问题,我认为Potatoswatter的第二个答案很有价值:“只需在您自己的命名空间中声明一个新的endl,或者更好的是,一个根本不被称为endl的操作器!”因此,我找到了如何编写自定义操作器的方法,这并不难:
#include <sstream>
#include <iostream>

class log_t : public std::ostringstream
{
    public:
};


std::ostream& custom_endl(std::ostream& out)
{
    log_t *log = dynamic_cast<log_t*>(&out);
    if (log)
    {
        std::cout << "custom endl succeeded.\n";
    }
    out << std::endl;
    return out;
}

std::ostream& custom_flush(std::ostream& out)
{
    log_t *log = dynamic_cast<log_t*>(&out);
    if (log)
    {
        std::cout << "custom flush succeeded.\n";
    }
    out << std::flush;
    return out;
}

int main(int argc, char **argv)
{
    log_t log;

    log << "custom endl test" << custom_endl;
    log << "custom flush test" << custom_flush;

    std::cout << "Contents of log:\n" << log.str() << std::endl;
}

这是输出的结果:
custom endl succeeded.
custom flush succeeded.
Contents of log:
custom endl test
custom flush test

我创建了两个自定义操作符,一个处理endl,另一个处理flush。你可以添加任何你想要的处理到这两个函数中,因为你有一个指向log_t对象的指针。


0

你不能改变 std::endl - 正如它的名字所示,它是 C++ 标准库的一部分,其行为是固定的。当流接收到一行结束时,你需要改变流本身的行为。个人认为这不值得花费精力,但如果你想涉足这个领域,我强烈建议阅读书籍 Standard C++ IOStreams & Locales


好书。但不是我推荐给初学者的一本书。但如果你想学习如何操作流类,它是必读的。 - Martin York
2
@Martin 我也不会建议初学者去写专业流,因为这不是初学者的任务。 - anon
1
同意: <-----15个字符------> :-) - Martin York

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接