【问题标题】:Confused about usage of `std::istreambuf_iterator`对`std::istreambuf_iterator`的使用感到困惑
【发布时间】:2011-05-08 05:31:58
【问题描述】:

我已经使用<< 流运算符为对象实现了反序列化例程。该例程本身使用istreambuf_iterator<char> 从流中逐个提取字符,以构造对象。

最终,我的目标是能够使用istream_iterator<MyObject> 遍历流并将每个对象插入vector。相当标准,除了当istream_iterator 到达流尾时,我无法stop 进行迭代。现在,它只是永远循环,即使对 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&lt;char&gt;(ifs)istreambuf_iterator&lt;char&gt;() 比较为true,这意味着我肯定在流的末尾。

我查看了 IOstreams 库代码,以确切了解它如何确定 istream_iterator 是否位于结束位置,并且基本上它检查 istream::operator void*() const 的计算结果是否为 true。这个 istream 库函数简单地返回:

return this->fail() ? 0 : const_cast<basic_ios*>(this);

换句话说,如果设置了故障位,它将返回0 (false)。然后,它将这个值与istream_iterator 的默认构造实例中的相同值进行比较,以确定我们是否结束了。

所以当istreambuf_iterator 与结束迭代器比较为真时,我尝试在std::istream&amp; operator &gt;&gt; (std::istream&amp; is, Foo&amp; f) 例程中手动设置故障位。这完美地工作,并正确终止了循环。但现在我真的很困惑。似乎istream_iterator 肯定 检查std::ios::failbit 以表示“流结束”条件。但这不是std::ios::eofbit 的用途吗?我认为failbit 是针对错误情况,例如无法打开fstream 的基础文件或其他情况。

那么,为什么我需要调用istream::setstate(std::ios::failbit) 来终止循环?

【问题讨论】:

  • 永远循环表明流已经坏了。问题是为什么?
  • @Martin,好吧,即使我用std::stringstream 替换文件流,也会出现同样的问题。所以这不可能是某种低级文件相关的问题。
  • 阅读@PigBen 的答案。原因是在外部级别,您在内部使用 istream_iterator(在 for_each 中)和 istreambuf_iterator(operatro >>)。您需要在使用中保持一致。在这两种情况下都使用 istreambuf_iterators,它应该可以工作。

标签: c++ iostream istream istream-iterator


【解决方案1】:

当您使用 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 对象上。如您所见here,操作员!只有在设置了 failbit 或 badbit 时才返回 true,eofbit 不会这样做。

那么,接下来会发生什么,如果设置了failbit 或badbit,则istream 指针将变为NULL。并且下一次将迭代器与结束迭代器进行比较时,它会比较 istream 指针,这两个指针都为 NULL。

【讨论】:

  • 我真的更喜欢使用 istreambuf_iterator,因为它允许我在其他类型的迭代器中通用地重用相同的例程。 (例如,当我的对象存储在std::string 中时,我可以使用string::iterator 反序列化它。)但我明白你在说什么——两组迭代器没有通信。那么,当istreambuf_iterator 到达末尾时,如果我只是在istream 对象上手动调用istream::setstate(std::ios::eofbit),为什么它不起作用?
  • 我猜这是因为当您将迭代器与流尾迭代器进行比较时,它会检查失败位而不是 eofbit。这样做是有意义的,因为在 istream 对象的正常操作中(使用 operator>>),每当设置 eofbit 时,也会设置 failbit。然而,反过来并不总是正确的,因此检查故障位更有意义。不过,在您的函数中,您应该模仿 operator>> 的行为并同时设置两者。
【解决方案2】:

你的外部循环——你正在检查你的istream_iterator 是否已经结束——与存储在istream 继承的ios_base 中的状态相关联。 istream 上的状态表示最近针对istream 本身执行的提取操作的结果,而不是其底层streambuf 的状态。

您的内部循环(您使用istreambuf_iteratorstreambuf 中提取字符的地方)使用较低级别的函数,例如basic_streambuf::sgetc()(用于operator*)和basic_streambuf::sbumpc()(用于operator++) .这些函数都没有设置状态标志作为副作用,除了第二个推进basic_streambuf::gptr

您的内部循环工作正常,但它是以一种偷偷摸摸的方式封装的,它违反了the contract of std::basic_istream&amp; operator&gt;&gt;(std::basic_istream&amp;, T&amp;)。如果函数未能按预期提取元素,则必须调用basic_ios::setstate(badbit),如果在提取时还遇到流尾,则还必须调用basic_ios::setstate(eofbit)。您的提取器函数在提取 Foo 失败时不会设置任何标志。

我同意这里的其他建议,以避免使用istreambuf_iterator 来实现旨在在istream 级别工作的提取运算符。你强迫自己做额外的工作来维护istream 合同,这将导致其他下游意外,就像把你带到这里的那个。

【讨论】:

    【解决方案3】:

    在您未能成功读取Foo 时,您应该在operator&gt;&gt; 中设置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,在检测到文件的eof时设置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 也会跳过前导空格。这可能是也可能不是您想要的。如果您不希望哨兵跳过前导空格,您可以使用以下方法构建它:

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

    如果sentry 在跳过前导空格时检测到文件结尾,它将同时设置failbiteofbit

    【讨论】:

      【解决方案4】:

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

      我得到了它的工作:

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

      【讨论】:

      • 好的 - 去 PigBen 看我的 cmets。我真的更喜欢使用std::istreambuf_iterator,因为使用迭代器可以让我编写可以在任何容器上运行的通用例程,而不是仅适用于流的例程。
      【解决方案5】:

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

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

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

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

      我认为这两个会产生相同的效果,但是(对我而言)while 循环更清晰。

      注意:您还有许多其他地方使用比较运算符来检查迭代器是否位于 eof。所有这些都应该改用.equal()

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-23
        • 2023-02-05
        相关资源
        最近更新 更多