【问题标题】:Implement reading from stream via copy通过复制实现从流中读取
【发布时间】:2013-01-15 09:04:07
【问题描述】:

我有一个表示字符序列的类,我想为它实现一个operator >>。我的实现目前看起来像这样:

inline std::istream& operator >>(std::istream& in, seq& rhs) {
    std::copy(
        std::istream_iterator<char>(in),
        std::istream_iterator<char>(),
        std::back_inserter(rhs));
    // `copy` doesn't know when to stop reading so it always also sets `fail`
    // along with `eof`, even if reading succeeded. On the other hand, when
    // reading actually failed, `eof` is not going to be set.
    if (in.fail() and in.eof())
        in.clear(std::ios_base::eofbit);
    return in;
}

但是,可以预见的是以下失败:

std::istringstream istr("GATTACA FOO");
seq s;
assert((istr >> s) and s == "GATTACA");

特别是,一旦我们到达“GATTACA FOO”中的空间,复制停止(预期)并在istream 上设置故障位(也是预期的)。然而,就seq而言,读取操作实际上是成功的。

我可以使用std::copy 对此进行建模吗?我也想过改用istreambuf_iterator,但这实际上并不能解决这个特殊问题。

此外,对输入“GATTACAFOO”的读取操作应该失败,因为该输入不代表有效的 DNA 序列(这是我的类所代表的)。另一方面,在 C++ 中从输入 42foo 中读取 int 实际上 成功 所以也许我应该将每个有效前缀都视为有效输入?

(顺便说一句,如果使用显式循环,这将相当简单,但我试图避免显式循环以支持算法。)

【问题讨论】:

  • 如果循环比算法更简单(因为否则你不会问这个问题),那么就用一个该死的循环。可维护性。
  • @Cat 我没有说“更直接”。事实上,我声称当使用适当的算法时(如果存在适当的序列适配器),算法解决方案总是比使用循环更直接。
  • 您可以使用模板,并且您可以以某种方式找到一种方法来使用多重继承、线程和您可以梦想的任何东西,但这只会让您的生活变得更加艰难,并且不会给您带来任何明显的好处.那么,为什么要强迫自己使用明显不适合手头任务需求的迭代器和算法呢?保持简单,使用那个简单的循环并继续解决真正的问题。不要用 15 行变通方法来搞乱您的项目,以获取一个旨在光滑和优雅的库解决方案。
  • @Arne 哇,吃冰镇药,伙计。我没有做任何你指责我的事情。我明确地问是否我的方法完全合适。但是,如果你认为这种方法“显然 [不] 合适”,那么我认为你不能做出太多贡献。否则为什么首先存在输入迭代器适配器?
  • @KonradRudolph 对不起,如果我夸大了那个。我的意思是:如果您必须努力获得序列适配器,那么算法不会更直接。在您的情况下,您必须解析输入以验证它。解析不适合普通的迭代算法,所以我怀疑你会在 &lt;algorithm&gt; 中找到任何合适的东西。

标签: c++ io istream istream-iterator


【解决方案1】:

您不想clear(eofbit),因为如果由于到达 EOF 而读取失败,failbit 应该保持设置。否则,如果您只是将eofbit 设置为没有failbit,则while (in &gt;&gt; s) 之类的循环将在到达EOF 后尝试再次读取,然后that read 将再次设置failbit。除非它正在使用您的operator&gt;&gt;,否则它会清除它,并尝试再次阅读。然后再次。然后再次。如果由于 EOF 读取失败,则流的正确行为是设置 failbit,因此只需保持设置即可。

要使用迭代器和算法来做到这一点,您需要类似

copy_while(InputIter, InputIter, OutputIter, Pred);

仅当谓词为真时才会复制输入序列,但这在标准库中不存在。不过你当然可以写一个。

template<typename InputIter, typename OutputIter, typename Pred>
  OutputIter
  copy_while(InputIter begin, InputIter end, OutputIter result, Pred pred)
  {
    while (begin != end)
    {
      typename std::iterator_traits<InputIter>::value_type value = *begin;
      if (!pred(value))
        break;
      *result = value;
      result++;
      begin++;
    }
    return result;
  }

现在你可以像这样使用它:

inline bool
is_valid_seq_char(char c)
{ return std::string("ACGT").find(c) != std::string::npos; }

inline std::istream&
operator>>(std::istream& in, seq& rhs)
{
    copy_while(
        std::istream_iterator<char>(in),
        std::istream_iterator<char>(),
        std::back_inserter(rhs),
        &is_valid_seq_char);
    return in;
}

int main()
{
    std::istringstream istr("GATTACA FOO");
    seq s;
    assert((istr >> s) and s == "GATTACA");
}

这可行,但问题是istream_iterator 使用operator&gt;&gt; 来读取字符,因此它会跳过空格。这意味着"GATTACA" 后面的空间被算法消耗并丢弃,因此将其添加到main 的末尾会失败:

assert(istr.get() == ' ');

要解决此问题,请使用不跳过空格的 istreambuf_iterator

inline std::istream&
operator>>(std::istream& in, seq& rhs)
{
    copy_while(
        std::istreambuf_iterator<char>(in),
        std::istreambuf_iterator<char>(),
        std::back_inserter(rhs),
        &is_valid_seq_char);
    return in;
}

要完成此操作,如果没有提取字符,您可能希望指示提取 seq 失败:

inline std::istream&
operator>>(std::istream& in, seq& rhs)
{
    copy_while( std::istreambuf_iterator<char>(in), {},
        std::back_inserter(rhs), &is_valid_seq_char);
    if (seq.empty())
      in.setstate(std::ios::failbit);  // no seq in stream
    return in;
}

最终版本还使用了我最喜欢的 C++11 技巧之一,通过使用 {} 作为结束迭代器来稍微简化它。 copy_while 的第二个参数的类型必须与第一个参数的类型相同,推导出为 std::istreambuf_iterator&lt;char&gt;,因此 {} 只是对另一个相同类型的迭代器进行值初始化。

编辑:如果您想更接近地匹配std::string 提取,那么您也可以这样做:

inline std::istream&
operator>>(std::istream& in, seq& rhs)
{
    std::istream::sentry s(in);
    if (s)
    {
        copy_while( std::istreambuf_iterator<char>(in), {},
                    std::back_inserter(rhs), &is_valid_seq_char);
        int eof = std::char_traits<char>::eof();
        if (std::char_traits<char>::eq_int_type(in.rdbuf()->sgetc(), eof))
            in.setstate(std::ios::eofbit);
    }
    if (rhs.empty())
        in.setstate(std::ios::failbit);
    return in;
}

哨兵将跳过前导空格,如果您到达输入的末尾,它将设置eofbit。可能应该进行的另一个更改是在将任何内容推入之前清空seq,例如以 rhs.clear() 或您的 seq 类型的等效项开头。

【讨论】:

  • 我原本要在问题中提到我不能使用istreambuf_iterator,因为我实际上并没有读取流的底层 char 类型,我正在读取自定义类型(对应于单个unsigned char) 我需要使用该类型的流输入运算符。我想用noskipws 解决问题,但我暂时放弃了。
  • 太好了,copy_while 正是我在评论中所想的。也为 {} 技巧 +1。
  • @KonradRudolph 为什么不从读取 char 的迭代器序列中读取并输出到写入您的类型的迭代器?如果有一个应该工作的隐式转换。
  • @Arne 没有隐式转换,如果值无效,显式转换将抛出,而不是将流置于错误状态。但实际上,一旦我切换到使用take_while,后者根本不是问题,并且我可以解决转换问题。谢谢提示,我试试看。
  • 实际上,这段代码并不完全适合我。在您的示例中,阅读后std::cin.eof() == false。另一方面,如果我正在阅读std::stringeof 将是trueIllustrative code
【解决方案2】:

特别是,一旦我们到达“GATTACA FOO”中的空间,复制 停止(预期)

这个假设已经是错误的。相反,你没有得到

std::istringstream istr("GATTACA FOO");
seq s;
assert(!(istr >> s) && s == "GATTACAFOO");

使用istream_iterator&lt;char&gt; 和标准copy 算法进行复制是行不通的,因为这将始终提取字符直到流结束。

如果达到结束条件并且结束条件不能提取不匹配的字符(即使用in.peek() 甚至直接查看streambuf),您需要一个提前终止的副本。

使用std::copy() 这样做需要您自己的专用流迭代器(如果终止条件与下一个字符匹配,则比较等于结束迭代器。恕我直言,这比显式循环更晦涩难懂。YMMV

【讨论】:

  • 对,不要使用整个输入,然后使用流状态标志,使用有效序列(这意味着在进行时检查并在有效序列的末尾停止,例如 @987654326 @case,当有疑问时,像 ints 那样做!)并且仅在您未能提取任何有效序列时手动更改流状态(因为当流中没有序列时调用 operator&gt;&gt; 重载来提取序列是失败的.) 到达EOF时让流管理eofbit,不要提前设置,到达EOF时不要清除
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-28
  • 1970-01-01
  • 2013-11-08
  • 1970-01-01
相关资源
最近更新 更多