`std::istreambuf_iterator` 的使用让我感到困惑。

3
我使用了 << 流操作符为一个对象实现了反序列化程序。该程序本身使用 istreambuf_iterator 从流中逐个提取字符,以构建对象。
最终,我的目标是能够使用 istream_iterator 迭代流,并将每个对象插入 vector 中。这很常见,但我在让 istream_iterator 在到达流的结尾时停止迭代方面遇到了麻烦。目前,它只是无限循环,即使调用 istream::tellg() 表明我已经到达了文件的末尾。
以下是重现问题的代码:
struct Foo
{
    Foo() { }    
    Foo(char a_, char b_) : a(a_), b(b_) { }

    char a;
    char b;
};

// Output stream operator
std::ostream& operator << (std::ostream& os, const Foo& f)
{
    os << f.a << f.b;
    return os;
}

// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    if (is.good()) 
    {
        std::istreambuf_iterator<char> it(is);
        std::istreambuf_iterator<char> end;

        if (it != end) {
            f.a = *it++;
            f.b = *it++;
        }
    }
    return is;
}

int main()
{
    {
        std::ofstream ofs("foo.txt");
        ofs << Foo('a', 'b') << Foo('c', 'd');
    }

    std::ifstream ifs("foo.txt");
    std::istream_iterator<Foo> it(ifs);
    std::istream_iterator<Foo> end;
    for (; it != end; ++it) cout << *it << endl; // iterates infinitely
}

我知道在这个简单的例子中,我甚至不需要 istreambuf_iterator,但是我只是试图简化问题,以便更有可能得到人们的答案。
所以这里的问题是,即使 istreambuf_iterator 到达了流缓冲区的末尾,实际的流本身也没有进入 EOF 状态。 调用 istream::eof() 返回 false,即使 istream::tellg() 返回文件中的最后一个字节,istreambuf_iterator(ifs) 与 istreambuf_iterator() 相比为 true,这意味着我肯定已经到达了流的末尾。
我查看了 IOstreams 库代码,以确定它如何确定一个 istream_iterator 是否处于末尾位置,基本上它检查 istream::operator void*() const 是否评估为 true。这个 istream 库函数简单地返回:
return this->fail() ? 0 : const_cast<basic_ios*>(this);

换句话说,如果设置了failbit,则返回0(false)。 然后,将此值与默认构造的istream_iterator实例中的相同值进行比较,以确定我们是否到达末尾。
因此,当istreambuf_iterator与结束迭代器比较为true时,我尝试在std :: istream&operator >>(std :: istream&is,Foo&f)例程中手动设置failbit。 这完美地运作并正确终止了循环。 但现在我真的很困惑。 似乎istream_iterator明确检查std :: ios :: failbit以表示“流结束”条件。 但这不是std :: ios :: eofbit的用途吗? 我认为failbit是用于错误条件的,例如如果无法打开fstream的底层文件之类的情况。
那么,为什么需要调用istream :: setstate(std :: ios :: failbit)才能终止循环呢?

循环永远表示流已经出现问题。问题是为什么? - Martin York
@Martin,即使我用std::stringstream替换文件流,仍然会出现相同的问题。因此,这不可能是某种低级文件相关的问题。 - Channel72
阅读@PigBen的回答。原因是你在外层使用了istream_iterator(在for_each中),而在内部使用了istreambuf_iterator(operator >>)。你需要在使用上保持一致。在两种情况下都使用istreambuf_iterators,应该就可以工作了。 - Martin York
5个回答

7
当您使用istreambuf_iterator时,您正在操作istream对象的底层streambuf对象。streambuf对象不知道它的所有者(istream对象),因此在streambuf对象上调用函数不会对istream对象进行更改。这就是为什么当您到达eof时,istream对象中的标志未设置的原因。
像这样做:
std::istream& operator >> (std::istream& is, Foo& f)
{
    is.read(&f.a, sizeof(f.a));
    is.read(&f.b, sizeof(f.b));
    return is;
}

编辑

我在调试器中逐步执行代码时发现了以下情况。istream_iterator有两个内部数据成员:指向关联的istream对象的指针和模板类型的对象(在此示例中为Foo)。当您调用++it时,它会调用此函数:

void _Getval()
{    // get a _Ty value if possible
    if (_Myistr != 0 && !(*_Myistr >> _Myval))
        _Myistr = 0;
}

_Myistr 是 istream 指针,_Myval 是 Foo 对象。如果您在这里查看:

!(*_Myistr >> _Myval)

那就是它调用了你的operator>>重载函数。然后它在返回的istream对象上调用operator!。正如你可以看到的这里,operator!只有在设置了failbit或badbit时才返回true,而eofbit则不行。
所以,如果设置了failbit或badbit中的任意一个,istream指针将被置为NULL。下一次当你将迭代器与结束迭代器进行比较时,它会比较istream指针,而它们都为NULL。

我真的更喜欢使用istreambuf_iterator,因为它允许我通用地重复使用相同的例程与其他类型的迭代器。 (例如,当我的对象存储在“std :: string”中时,我可以使用“string :: iterator”对其进行反序列化。)但我知道你的意思-这两组迭代器没有沟通。那么,如果我手动调用istream :: setstate(std :: ios :: eofbit)来处理istreambuf_iterator到达结尾时的istream对象,为什么不起作用? - Channel72
我猜这是因为当你将迭代器与流的结束迭代器进行比较时,它会检查 failbit 而不是 eofbit。这样做是有道理的,因为在 istream 对象的正常操作中(使用 operator>>),每当设置 eofbit 时,也会设置 failbit。然而,反过来并不总是成立,所以检查 failbit 更有意义。但是,在您的函数中,您应该模仿 operator>> 的行为并同时设置两者。 - Benjamin Lindley

4
您的外部循环——在其中检查您的istream_iterator是否已达到其结尾——与存储在istream继承的ios_base中的状态相关联。 istream上的状态表示最近针对istream本身执行的提取操作的结果,而不是其底层streambuf的状态。
你的内循环——使用istreambuf_iteratorstreambuf中提取字符——正在使用低级别函数,如basic_streambuf::sgetc()(用于operator*)和basic_streambuf::sbumpc()(用于operator++)。除了第二个函数将basic_streambuf::gptr前进之外,这些函数都不会在副作用中设置状态标志。
你的内部循环运行良好,但它被包装成一种狡猾的方式实现,并违反了 std::basic_istream& operator>>(std::basic_istream&, T&)的契约。如果该函数未能按预期提取元素,则必须调用basic_ios::setstate(badbit),如果在提取过程中也遇到了流结束,则还必须调用basic_ios::setstate(eofbit)。当你的提取函数无法提取Foo时,它既不设置badbit标志,也不设置eofbit标志。
我赞同这里其他建议,避免使用istreambuf_iterator来实现在istream级别工作的提取运算符。你强迫自己做额外的工作来维护istream的契约,这会导致其他下游惊喜,就像你在这里遇到的那个问题一样。

2

在你的operator>>中,每当读取Foo失败时,应该设置failbit。此外,每当检测到文件结束时,应该设置eofbit。可能看起来像这样:

// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    if (is.good()) 
    {
        std::istreambuf_iterator<char> it(is);
        std::istreambuf_iterator<char> end;

        std::ios_base::iostate err = it == end ? (std::ios_base::eofbit |
                                                  std::ios_base::failbit) :
                                                 std::ios_base::goodbit;
        if (err == std::ios_base::goodbit) {
            char a = *it;
            if (++it != end)
            {
                char b = *it;
                if (++it == end)
                    err = std::ios_base::eofbit;
                f.a = a;
                f.b = b;
            }
            else
                err = std::ios_base::eofbit | std::ios_base::failbit;
        }
        if (err)
            is.setstate(err);
    }
    else
        is.setstate(std::ios_base::failbit);
    return is;
}

使用这个提取器,如果读取失败则设置failbit,如果检测到文件结束则设置eofbit,这样你的驱动程序就能正常工作。请特别注意,即使外部的if (is.good())失败,你仍然需要设置failbit。你的流可能是!good(),因为只有eofbit被设置了。
你可以通过使用istream::sentry来稍微简化上述过程。如果sentry失败,它会自动为您设置failbit
// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    std::istream::sentry ok(is);
    if (ok) 
    {
        std::istreambuf_iterator<char> it(is);
        std::istreambuf_iterator<char> end;

        std::ios_base::iostate err = it == end ? (std::ios_base::eofbit |
                                                  std::ios_base::failbit) :
                                                 std::ios_base::goodbit;
        if (err == std::ios_base::goodbit) {
            char a = *it;
            if (++it != end)
            {
                char b = *it;
                if (++it == end)
                    err = std::ios_base::eofbit;
                f.a = a;
                f.b = b;
            }
            else
                err = std::ios_base::eofbit | std::ios_base::failbit;
        }
        if (err)
            is.setstate(err);
    }
    return is;
}

sentry还会跳过前导空格。这可能是您想要的,也可能不是。如果您不希望sentry跳过前导空格,则可以使用以下方式构造它:

    std::istream::sentry ok(is, true);

如果在跳过前导空格时,sentry检测到文件结束,则会设置failbiteofbit

1

看起来两组流迭代器互相干扰:

我用这个方法解决了问题:

// Input stream operator
std::istream& operator >> (std::istream& is, Foo& f)
{
    f.a = is.get();
    f.b = is.get();

    return is;
}

好的 - 看看我对PigBen的评论。我真的更喜欢使用std :: istreambuf_iterator,因为使用迭代器可以编写通用例程,可用于任何容器,而不是仅适用于流的例程。 - Channel72

0

我认为你的结束条件需要使用.equal()方法,而不是使用比较运算符。

for (; !it.equal(end); ++it) cout << *it << endl;

我通常看到这个用while循环来实现,而不是for循环:

while ( !it.equal(end)) {
    cout << *it++ << endl;
}

我认为这两种方法的效果是相同的,但是 while 循环更加清晰。

注意:您还有其他几个地方在使用比较运算符来检查迭代器是否到达了 eof。所有这些地方都应该改为使用 .equal()


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