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

时间:2012-01-11 16:23:10

标签: c++ parsing stream iostream std

我发现自己最近编写了很多解析代码(主要是自定义格式,但它并不是真的相关)。

为了增强可重用性,我选择将解析函数基于i / o流,以便我可以将它们与boost::lexical_cast<>之类的东西一起使用。

然而,我意识到我从来没有读过任何关于如何正确地做到这一点的事情。

为了说明我的问题,我们假设我有三个课程FooBarFooBar

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

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

FooBar是一种变体类型,可以包含FooBar

现在让我们说我为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;
}

解析适用于有效数据。但如果数据无效:

  • 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; 
}

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

所以这是我的问题:

  • 我的错误在哪里?
  • 是否应该将他的电话写入operator>>,以便在失败后保留初始数据?如果是这样,我怎么能有效地做到这一点?
  • 如果没有,是否有办法存储&#34; (并恢复)输入流的完整状态:状态数据?
  • failbitbadbit之间有何不同?我们应该何时使用其中一种?
  • 是否有任何在线参考(或书籍)深入解释如何处理iostreams?不只是基本的东西:完整的错误处理。

非常感谢。

3 个答案:

答案 0 :(得分:3)

就我个人而言,我认为这些都是合理的问题而且我很清楚我自己也在努力解决这些问题。所以我们走了:

  

我的错误在哪里?

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

让我们做这个练习,因为有些事情可能是并发症。根据您对格式的描述,如果&#34;字符串&#34;并且字符串后面的内容是分隔的,例如通过空格(空格,制表符等)。如果没有,你就不能只读std::string:它们的默认行为是读到下一个空格。有一些方法可以将流调整为将字符视为空格(使用std::ctype<char>),但我只假设有空格。在这种情况下,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);
}

我们的想法是read_data()在阅读Foo时读取Bar的与FooBar不同的部分。类似的方法将用于Bar,但我省略了这一点。更有趣的是使用这个有趣的get_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;
}

我的代码看起来有点奇怪,如果事情有效,几乎没有检查。这是因为在阅读会员时,流只会设置std::ios_base::failbit,而且我不必为此烦恼。实际上添加了特殊逻辑的唯一情况是get_char()来处理期望特定字符。类似地,没有跳过空格字符(即使用std::ws):所有输入函数都是formatted input函数,默认情况下跳过这些空格(你可以使用例如{{1}来关闭它但是很多事情都没有成功。

通过类似的阅读in >> std::noskipws的实现,阅读Bar看起来像这样:

FooBar

此代码使用无格式输入函数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; } ,它只查看下一个字符。它返回下一个字符,如果失败则返回peek()。因此,如果有左括号或左括号,我们会std::char_traits<char>::eof()接管。否则我们总是失败。解决了眼前的问题。关于分发信息......

  

是否应该将他的电话写入运营商&gt;&gt;失败后仍保留初始数据?

一般答案是:不。如果你没有看到出错的东西,你放弃了。但这可能意味着您需要更加努力地工作以避免失败。如果您真的需要退出解决数据的位置,可能需要先使用read_data()将数据读入std::string,然后分析此字符串。使用std::getline()假设有一个明确的角色可以停留。默认值是换行符(因此名称),但您也可以使用其他字符:

std::getline()

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

if(std :: getline(in,str)&amp;&amp; in.eof()){std :: cout&lt;&lt; &#34;文件未以换行符结尾\&#34 ;; }

  

如果是这样,我该如何有效地做到这一点?

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

如果你想要返回一个角色,你必须准确地放回你提取的角色:

std::getline()

后一个函数的性能稍好一些,因为它不必检查该字符确实是提取的字符。它失败的可能性也较小。理论上,您可以根据需要放回多个字符,但大多数流在所有情况下都不会支持多个:如果有缓冲区,标准库会处理&#34; ungetting&#34;到达缓冲区开始之前的所有字符。如果返回另一个字符,它将调用虚函数char c; if (in >> c && c != 'a') in.putback(c); if (in >> c && c != 'b') in.unget(); ,这可能会或可能不会提供更多缓冲区空间。在我实现的流缓冲区中,它通常会失败,即我通常不会覆盖此功能。

  

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

如果你的意思是完全恢复你所处的状态,包括角色,答案是:确定有。 ......但没有简单方式。例如,您可以实现过滤流缓冲区并按上述方式放回字符以恢复要读取的序列(或支持在流中寻找或明确设置标记)。对于某些流,您可以使用搜索但不是所有流都支持此功能。例如,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 复制与流相关的所有与格式相关的字段。这些是:

  • 语言环境
  • fmtflags
  • 信息存储iword()和pword()
  • 流媒体事件
  • 例外
  • 溪流的州

如果你不知道他们中的大多数人都不用担心:大多数你可能不会关心的东西。好吧,直到你需要它,但到那时你有希望获得一些文档并阅读它(或询问并得到一个很好的回应)。

  

failbit和badbit有什么区别?我们应该何时使用其中一种?

最后简短而简单:

    检测到格式错误时设置
  • copyfmt(),例如一个数字是预期的,但字符&#39; T&#39;找到了。
  • 当流的基础架构出现问题时,设置
  • failbit。例如,当未设置流缓冲区时(如上面的流badbit中),流已设置fmt。另一个原因是如果抛出异常(并通过std::badbit掩码捕获;默认情况下会捕获所有异常。)
  

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

啊,是的,很高兴你问。你可能想要得到Nicolai Josuttis&#34; The C ++ Standard Library&#34;。我知道这本书描述了所有的细节因为我写了它。如果你真的想知道关于IOStreams和locales的一切,你想要Angelika Langer&amp; Klaus Kreft&#34; IOStreams和Locales&#34;。如果你想知道我从哪里得到了最初的信息:这是Steve Teale&#34; IOStreams&#34;我不知道这本书是否仍在印刷中,并且缺乏标准化过程中引入的大量内容。由于我实现了自己的IOStreams(和语言环境)版本,我也知道扩展。

答案 1 :(得分:2)

  

所以这是我的问题:

     

问:我的错误在哪里?

我不会把你的技术称为错误。绝对没问题 当您从流中读取数据时,您通常已经知道从该流中传出的对象(如果对象有多种解释,那么还需要将其编码到流中(或者您需要能够回滚该流)。

  

问:是否应该将他的来电写入运营商&gt;&gt;失败后仍保留初始数据?

只有在出现严重问题的情况下才应该出现故障状态 在您的情况下,如果您期望foobar(有两个表示),您可以选择:

  1. 使用一些前缀数据标记流中的对象类型。
  2. 在foobar解析部分中,使用ftell()和fseek()恢复流位置。
  3. 尝试:

      std::streampos point = stream.tellg();
      if (is >> foo)
      {
        foobar = foo;
      }
      else
      {
        stream.seekg(point)
        is.clear();
    
      

    问:如果是这样,我该如何有效地做到这一点?

    我更喜欢方法1,你知道流上的类型 当这是不可知的时候,可以使用方法二。

      

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

    是的,但需要两次通话:see

    std::iostate   state = stream.rdstate()
    std::istream   holder;
    holder.copyfmt(stream)
    
      

    问: failbit和badbit之间有什么区别?

    从文档到调用失败():

      当错误与操作本身的内部逻辑相关时,

    failbit:通常由输入操作设置,因此可以对流进行其他操作。
       badbit:通常在错误涉及流的完整性丢失时设置,即使对流执行了不同的操作,也可能会持续存在。可以通过调用成员函数bad来独立检查badbit。

         

    问:我们什么时候应该使用其中一种?

    你应该设置failbit 这意味着您的操作失败。如果您知道它失败了,那么您可以重置并重试。

    badbit就是当你意外地混淆流的内部成员或做一些非常糟糕的事情时,流对象本身就完全分叉了。

答案 2 :(得分:0)

当你序列化你的FooBar时,你应该有一个标志,指出它是哪一个,这将是你的写/读的“标题”。

当你回读它时,你会读取标志,然后读入相应的数据类型。

是的,首先读入临时对象然后移动数据是最安全的。您有时可以使用swap()函数对其进行优化。