5

I am aware of the origin of this behavior since it has been very well explained in multiple posts here in SO, some notable examples are:

Why is iostream::eof inside a loop condition considered wrong?

Use getline() without setting failbit

std::getline throwing when it hits eof

C++ istream EOF does not guarantee failbit?

And it is also included in the std::getline standard:

3) If no characters were extracted for whatever reason (not even the discarded delimiter), getline sets failbit and returns.

My question is how does one deal with this behavior, where you want your stream to catch a failbit exception for all cases except the one caused by reaching the eof, of a file with an empty last line. Is there something obvious that I am missing?

A MWE:

#include <iostream>
#include <string>
#include <fstream>
#include <sstream>


void f(const std::string & file_name, char comment) {

std::ifstream file(file_name);
file.exceptions(file.failbit);
    try {
          std::string line;

          while (std::getline(file, line).good()) {
          // empty getline sets failbit throwing an exception
            if ((line[0] != comment) && (line.size() != 0)) {
                std::stringstream ss(line);
                // do stuff
            }
        }
    }

    catch (const std::ios_base::failure& e) {
        std::cerr << "Caught an ios_base::failure.\n"
        << "Explanatory string: " << e.what() << '\n'
        << "Error code: " << e.code() << '\n';

        }
}


int main() {

    f("example.txt", '#');
}

where example.txt is a tab-delimited file, with its last line being only the \n char:

# This is a text file meant for testing
0   9
1   8
2   7

EDIT:

while(std::getline(file, line).good()){...} replicates the problem.

nikjohn
  • 483
  • 9
  • 17

2 Answers2

3

Another way to avoid setting failbit, is simply to refactor your if tests to detect the read of an empty-line. Since that is your final line in this case, you can simply return to avoid throwing the error, e.g.:

    std::ifstream file (file_name);
    file.exceptions (file.failbit);
    try {
        std::string line;

        while (std::getline(file, line)) {
            // detect empty line and return
            if (line.size() == 0)
                return;
            if (line[0] != comment) {
                std::stringstream ss(line);
                // do stuff
            }
        }
    }
    ...

You other alternative is to check whether eofbit is set in catch. If eofbit is set -- the read completed successfully. E.g.

    catch (const std::ios_base::failure& e) {
        if (!file.eof())
            std::cerr << "Caught an ios_base::failure.\n"
            << "Explanatory string: " << e.what() << '\n'
            << "Error code: " /* << e.code() */ << '\n';
    }
David C. Rankin
  • 69,681
  • 6
  • 44
  • 72
  • From my understanding/testing the `line.size()==0` statement after the `std::getline(file,line)` will not work, because in the case where `getline` reads an empty line, it will set the `failbit`, exiting the `while` loop and causing the exception. The purpose of my `if (line.size() != 0)` is simply to avoid storing empty lines in the off-chance one exists in between the data in the file it's not for mitigating the exception throw. – nikjohn Jun 03 '18 at 04:37
  • When `getline` reads the line containing only `'\n'`, the `'\n'` is extracted, but not stored in `line`, `gcount = 1`, there is no `eofbit` or `failbit` set. When you detect the `line.size() = 0`, you know the only thing that would have been read is the `'\n'`. It works, I used your data file to test it `:)` You also avoid calling `line[0] != comment` until you have validated `line.size() != 0`. – David C. Rankin Jun 03 '18 at 04:50
  • 1
    @nikjohn - another simple fix is to include `if (!file.eof())` as the first line in `catch`. – David C. Rankin Jun 03 '18 at 05:31
  • That was my original approach but the statement had/has absolutely no effect in the MWE, which is strange to say the least – nikjohn Jun 03 '18 at 05:36
  • 1
    I suspect that is a side-effect of the UB @Arcinide picked up on caused by accessing `line[0] != comment` when `line` is empty. Turning your test around `line.size() != 0 && line[0] != comment` would avoid that too. – David C. Rankin Jun 03 '18 at 05:38
2

Edit: I misunderstood the OP, refer to David's answer above. This answer is for checking whether or not the file has a terminating newline.

At the end of your while (getline) loop, check for file.eof().

Suppose you just did std::getline() for the last line in the file.

  • If there is a \n after it, then std::getline() has read the delimiter and did not set eofbit. (In this case, the very next std::getline() will set eofbit.)

  • Whereas if there is no \n after it, then std::getline() has read EOF and did set eofbit.

In both cases, the very next std::getline() will trigger failbit and enter your exception handler.

PS: the line if ((line[0] != comment) && (line.size() != 0)) { is UB if line is empty. The conditions' order needs to be reversed.

jcai
  • 3,138
  • 3
  • 17
  • 32
  • 1
    I'm not sure checking `file.eof()` is correct. No matter the case (unless you add `peek()`) `getline()` will be the first to set `eofbit`, regardless of a check for `file.eof()` within the loop. I'm not sure I follow how you intend to implement the check? As it sits, the error reports `basic_ios::clear: iostream erro` and the code being `iostream:1` – David C. Rankin Jun 03 '18 at 04:12
  • @DavidC.Rankin If the file ends with a newline, a `getline` will set `eofbit`, then the very next `getline` will set `failbit`. If it does not, a single `getline` will send `eofbit` and `failbit` simultaneously. The check will catch this. I assume the OP just wants to distinguish these two cases. – jcai Jun 03 '18 at 05:12
  • Yes, yes I agree completely with your analysis of what happens, the only thing I questioned was how a `f.eof()` inside the loop would help. – David C. Rankin Jun 03 '18 at 05:31
  • @DavidC.Rankin Ah, I see what you are saying, and it seems I misunderstood the OP. Your solution is correct. – jcai Jun 03 '18 at 05:51