关于使用 iostream 进行解析的准则是什么?

ere*_*eOn 5 c++ parsing iostream std stream

我发现自己最近写了很多解析代码(大部分是自定义格式,但并不真正相关)。

为了增强可重用性,我选择将解析函数基于 i/o 流,以便我可以将它们与boost::lexical_cast<>.

然而,我意识到我从未在任何地方读过任何有关如何正确执行此操作的内容。

为了说明我的问题,让我们考虑一下我有三个类Foo,BarFooBar

AFoo由以下格式的数据表示:string(<number>, <number>)

ABar由以下格式的数据表示:string[<number>]

AFooBar是一种变体类型,可以容纳 aFoo或 a Bar

现在假设我operator>>()为我的Foo类型写了一个:

istream& operator>>(istream& is, Foo& foo)
{
    char c1, c2, c3;
    is >> foo.m_string >> c1 >> foo.m_x >> c2 >> std::ws >> foo.m_y >> c3;

    if ((c1 != '(') || (c2 != ',') || (c3 != ')'))
    {
      is.setstate(std::ios_base::failbit);
    }

    return is;
}
Run Code Online (Sandbox Code Playgroud)

对于有效数据,解析效果很好。但如果数据无效:

  • foo可能会被部分修改;
  • 输入流中的某些数据已被读取,因此不再可用于进一步调用is.

另外,我operator>>()为我的FooBar类型写了另一个:

istream& operator>>(istream& is, FooBar foobar)
{
  Foo foo;

  if (is >> foo)
  {
    foobar = foo;
  }
  else
  {
    is.clear();

    Bar bar;

    if (is >> bar)
    {
      foobar = bar;
    }
  }

  return is; 
}
Run Code Online (Sandbox Code Playgroud)

但显然它不起作用,因为如果is >> foo失败,一些数据已经被读取并且不再可用于调用is >> bar.

这是我的问题:

  • 我的错误在哪里?
  • 是否应该编写他的调用以operator>>使初始数据在失败后仍然可用?如果是这样,我怎样才能有效地做到这一点?
  • 如果没有,是否有办法“存储”(和恢复)输入流的完整状态:状态数据?
  • failbit和之间有什么区别badbit?我们什么时候应该使用其中之一?
  • 是否有任何在线参考资料(或一本书)深入解释如何处理 iostreams ?不仅仅是基本的东西:完整的错误处理。

非常感谢。

Die*_*ühl 4

就我个人而言,我认为这些都是合理的问题,而且我清楚地记得我自己也曾为这些问题而苦苦挣扎。那么我们开始吧:

我的错误在哪里?

我不会称其为错误,但您可能想确保不必放弃所读的内容。也就是说,我将实现三个版本的输入函数。根据特定类型解码的复杂程度,我什至可能不会共享代码,因为无论如何它可能只是一小部分。如果超过一两行,可能会共享代码。也就是说,在您的示例中,我将有一个提取器,FooBar它本质上读取FooBar成员并相应地初始化对象。或者,我会阅读主要部分,然后调用提取公共数据的共享实现。

让我们来做这个练习,因为有一些事情可能会变得复杂。根据您对格式的描述,我不清楚“字符串”以及字符串后面的内容是否由空格(空格、制表符等)分隔。如果没有,您不能只读取 a std::string:它们的默认行为是读取直到下一个空格。有多种方法可以调整流以将字符视为空白(使用std::ctype<char>),但我只是假设有空格。在这种情况下, for 的提取器Foo可能如下所示(注意,所有代码都完全未经测试):

std::istream& read_data(std::istream& is, Foo& foo, std::string& s) {
    Foo tmp(s);
    if (is >> get_char<'('> >> tmp.m_x >> get_char<','> >> tmp.m_y >> get_char<')'>)
        std::swap(tmp, foo);
    return is;
}
std::istream& operator>>(std::istream& is, Foo& foo)
{
    std::string s;
    return read_data(is >> s, foo, s);
}
Run Code Online (Sandbox Code Playgroud)

这个想法是read_data()读取 a 的部分与读取 a 时Foo不同。可以使用类似的方法,但我省略了这一点。更有趣的是这个有趣的函数模板的使用。这就是所谓的操纵器,只是一个以流引用作为参数并返回流引用的函数。由于我们想要读取和比较不同的字符,因此我将其作为模板,但每个字符也可以有一个函数。我只是懒得打字:BarFooBarBarget_char()

template <char Expect>
std::istream& get_char(std::istream& in) {
    char c;
    if (in >> c && c != 'e') {
        in.set_state(std::ios_base::failbit);
    }
    return in;
}
Run Code Online (Sandbox Code Playgroud)

我的代码看起来有点奇怪,因为几乎没有检查事情是否有效。这是因为当读取成员失败时流就会设置std::ios_base::failbit,我真的不必打扰自己。实际上添加特殊逻辑的唯一情况是get_char()处理期望特定字符。类似地,也不会跳过空白字符(即使用std::ws):所有输入函数都是formatted input函数,并且默认情况下这些函数会跳过空白(您可以使用 eg 将其关闭in >> std::noskipws),但很多事情都将不起作用。

使用类似的读取 a 的实现Bar,读取 aFooBar看起来像这样:

std::istream& operator>> (std::istream& in, FooBar& foobar) {
    std::string s;
    if (in >> s) {
         switch ((in >> std::ws).peek()) {
         case '(': { Foo foo; read_data(in, foo, s); foobar = foo; break; }
         case '[': { Bar bar; read_data(in, bar, s); foobar = bar; break; }
         default: in.set_state(std::ios_base::failbit);
         }
    }
    return in;
 }
Run Code Online (Sandbox Code Playgroud)

此代码使用未格式化的输入函数,peek()该函数仅查看下一个字符。std::char_traits<char>::eof()它要么返回下一个字符,要么如果失败则返回。因此,如果有左括号或左方括号,我们就read_data()接管。否则我们总是会失败。解决了眼前的问题。继续发布信息...

是否应该将其调用写入操作员>>以使初始数据在失败后仍然可用?

一般的答案是:不。如果你没能读懂,就会出现问题,你就会放弃。不过,这可能意味着您需要更加努力才能避免失败。如果您确实需要从原来的位置退出来解析数据,您可能需要先将数据读入 using std::stringstd::getline()然后分析该字符串。使用std::getline()假设有一个明确的字符要停止。默认值是换行符(因此得名),但您也可以使用其他字符:

std::getline(in, str, '!');
Run Code Online (Sandbox Code Playgroud)

这将在下一个感叹号处停止并将其之前的所有字符存储在 中str。它还会提取终止字符,但不会存储它。有时,当您读取可能没有换行符的文件的最后一行时,这会变得很有趣:std::getline()如果它可以读取至少一个字符,则成功。如果您需要知道文件中的最后一个字符是否是换行符,您可以测试流是否到达:

if (std::getline(in, str) && in.eof()) { std::cout << "文件不以换行符结尾\"; }

如果是这样,我怎样才能有效地做到这一点?

流本质上是单次传递:您只接收每个字符一次,如果您跳过一个字符,则会消耗它。因此,您通常希望以不必回溯的方式构建数据。也就是说,这并不总是可能的,大多数流实际上在引擎盖下都有一个缓冲区,可以返回两个字符。由于流可以由用户实现,因此不能保证可以返回字符。即使对于标准流也没有真正的保证。

如果你想返回一个字符,你必须准确地放回你提取的字符:

char c;
if (in >> c && c != 'a')
    in.putback(c);
if (in >> c && c != 'b')
    in.unget();
Run Code Online (Sandbox Code Playgroud)

后一个函数的性能稍好一些,因为它不必检查字符是否确实是提取的字符。它失败的机会也更少。理论上,您可以放回任意数量的字符,但大多数流在所有情况下都不会支持多个字符:如果有缓冲区,标准库会负责“取消获取”所有字符,直到缓冲区开始到达了。如果返回另一个字符,它会调用虚拟函数,std::streambuf::pbackfail()该函数可能会也可能不会提供更多可用的缓冲区空间。在我实现的流缓冲区中,它通常会失败,即我通常不会覆盖此函数。

如果没有,是否有办法“存储”(和恢复)输入流的完整状态:状态和数据?

如果你想完全恢复你当时的状态,包括角色,答案是:当然有。...但没有简单的方法。例如,您可以实现过滤流缓冲区并按上述方式放回字符以恢复要读取的序列(或支持在流中查找或显式设置标记)。对于某些流,您可以使用查找,但并非所有流都支持此功能。例如,std::cin通常不支持查找。

不过,恢复角色只是故事的一半。您想要恢复的其他内容是状态标志和任何格式化数据。事实上,如果流进入失败甚至糟糕的状态,您需要在流执行大多数操作之前清除状态标志(尽管我认为格式化内容无论如何都可以重置):

std::istream fmt(0); // doesn't have a default constructor: create an invalid stream
fmt.copyfmt(in);     // safe the current format settings
// use in
in.copyfmt(fmt);     // restore the original format settings
Run Code Online (Sandbox Code Playgroud)

该函数copyfmt()复制与流关联的所有与格式相关的字段。这些都是:

  • 语言环境
  • fmt 标志
  • 信息存储 iword() 和 pword()
  • 流的事件
  • 例外情况
  • 流的状态

如果您不了解其中的大多数内容,请不要担心:大多数内容您可能不会关心。好吧,直到您需要它为止,但到那时您已经希望获得一些文档并阅读它(或询问并得到良好的答复)。

failbit 和 badbit 之间有什么区别?我们什么时候应该使用其中之一?

最后是一个简短而简单的:

  • failbit当检测到格式错误时设置,例如需要数字但发现字符“T”。
  • badbit当流的基础设施出现问题时设置。例如,当未设置流缓冲区时(如fmt上面的流所示),流已std::badbit设置。另一个原因是如果抛出异常(并通过掩码捕获exceptions();默认情况下捕获所有异常)。

是否有任何在线参考资料(或一本书)深入解释如何处理 iostreams ?不仅仅是基本的东西:完整的错误处理。

啊,是的,很高兴你问。您可能想要获取 Nicolai Josuttis 的“C++ 标准库”。我知道这本书描述了所有细节,因为我参与了这本书的写作。如果您确实想了解有关 IOStreams 和区域设置的所有信息,那么您需要 Angelika Langer 和 Klaus Kreft 的“IOStreams 和区域设置”。如果您想知道我最初从哪里获得信息:这是 Steve Teale 的“IOStreams”,我不知道这本书是否仍在印刷,并且它缺少标准化过程中引入的很多内容。由于我实现了自己的 IOStreams 版本(和语言环境),所以我也了解这些扩展。