检查流是否以换行符结尾

Check if a stream ends with a newline

我想检查流(实际上是 ifstream)是否以换行符结尾。我想出了这个:

bool StreamEndsWithNewline(std::basic_istream<char> & the_stream)
{
    if (the_stream.peek() == EOF) {
        the_stream.clear(); //clear flags set by peek()
        return false;
    }
    std::string line = "blah";
    while (std::getline(the_stream, line)) {
       // ...
    }
    return line.empty();
}

想法是,如果流的最后一行有一个 \n 结束字符,while 循环将进行一次额外的迭代(因为尚未达到 eof),其中将分配空字符串到行参数。

必须单独处理 "empty" 流的特殊情况。

它似乎适用于 windows (vs2010)。一般这样可以吗?

tldr;是的,这保证有效,除非流最初是空的。


有两个位需要考虑:fail 位和 eof 位。 std::getline 确实如此,来自 [string.io]:

After constructing a sentry object, if the sentry converts to true, calls str.erase() and then extracts characters from is and appends them to str as if by calling str.append(1, c) [...] If the function extracts no characters, it calls is.setstate(ios::failbit)

sentry 确实如此,来自 [istream::sentry]:

Effects: If is.good() is false, calls is.setstate(failbit). Otherwise, prepares for formatted or unformatted input. [...] If is.rdbuf()->sbumpc() or is.rdbuf()->sgetc() returns traits::eof(), the function calls setstate(failbit | eofbit)

考虑到所有这些,让我们来看两个例子:


案例 1:"hello\n"。第一次调用 getline(), the_stream.good() 为真,我们通过 \n 向上提取字符,流仍然是 good(),我们进入循环体 line 设置为 "hello"

第二次调用getline(),流还是good(),所以sentry对象转换为true,我们调用str.erase()。尝试提取后续字符失败,因为我们已经完成了流,因此设置了 failbit。这会导致 return getline() 转换为 false,因此我们不会再次进入循环体。在循环结束时,line 为空。


情况 2:"goodbye",没有换行符。第一次调用 getline()the_stream.good() 为真,我们提取字符直到我们命中 eof()。流 failbit 尚未设置,所以我们仍然进入循环体,行设置为 "goodbye"

第二次调用 getline()sentry 对象的构造失败,因为 is.good() 为假(is.good() 同时检查 eofbitfailbit)。由于这次失败,我们不进入调用 str.erase()getline() 的第一步。由于这次失败,failbit 被设置,所以我们再次不进入循环体。

循环结束时,line仍然是"goodbye"


案例 3:""。这里,getline() 将不提取任何字符,因此设置了 failbit 并且永远不会进入循环,并且 line 始终为空。有几种方法可以将这种情况与情况 1 区分开来:

  • 您可以预先 peek() 查看第一个字符是否为 traits::eof(),然后再执行任何其他操作。
  • 您可以计算进入循环的次数并检查它是否为非零。
  • 您可以将 line 初始化为某个标记非空值。在循环结束时,只有当流以定界符结束时,该行才会为空。

您的代码有效。

但是,您可以尝试搜索流并只测试最后一个字符或丢弃读取的字符:

#include <cassert>
#include <iostream>
#include <limits>
#include <sstream>

bool StreamEndsWithNewline(std::basic_istream<char>& stream) {
    const auto Unlimited = std::numeric_limits<std::streamsize>::max();
    bool result = false;
    if(stream) {
        if(std::basic_ios<char>::traits_type::eof() != stream.peek()) {
            if(stream.seekg(-1, std::ios::end)) {
                char c;
                result = (stream.get(c) && c == '\n');
                stream.ignore(Unlimited);
            }
            else {
                stream.clear();
                while(stream && stream.ignore(Unlimited, '\n')) {}
                result = (stream.gcount() == 0);
            }
        }
        stream.clear();
    }
    return result;
}

int main() {
    std::cout << "empty\n";
    std::istringstream empty;
    assert(StreamEndsWithNewline(empty) == false);

    std::cout << "empty_line\n";
    std::istringstream empty_line("\n");
    assert(StreamEndsWithNewline(empty_line) == true);

    std::cout << "line\n";
    std::istringstream line("Line\n");
    assert(StreamEndsWithNewline(line) == true);

    std::cout << "unterminated_line\n";
    std::istringstream unterminated_line("Line");
    assert(StreamEndsWithNewline(unterminated_line) == false);

    std::cout << "Please enter ctrl-D: (ctrl-Z on Windows)";
    std::cout.flush();
    assert(StreamEndsWithNewline(std::cin) == false);
    std::cout << '\n';

    std::cout << "Please enter Return and ctrl-D (ctrl-Z on Windows): ";
    std::cout.flush();
    assert(StreamEndsWithNewline(std::cin) == true);
    std::cout << '\n';

    return 0;
}