4

I want to do read from a file using C++ standard library facilities (std::ifstream) - while, of course, reliably reporting errors if I encounter them.

Apparently, this is not at all an easy task!

  • std::basic_fstream's (the template of which std::ifstream is an instantiation) do not throw exceptions by default.
  • You can make a basic fstream throw exceptions - but only after construction, so the construction won't fail. See basic_ios::exceptions() (that's a super-superclass of std::ifstream).

14 years ago, this question was asked:

Get std::fstream failure error messages and/or exceptions

and the answers tell us that:

  1. There are no guarantees the thrown exception will tell us what the cause of the error was (just that some error occurred)
  2. We are not guaranteed that when a failbit or badbit is set on an fstream, errno / GetLastError() provide us with a non-zero/non-success value.

That's quite bad. On the other hand, 14 years have passed. Has anything changed? That is, are there better guarantees regarding the thrown exception or of errno / GetLastError() being set? If not, what is the "best effort" approach to reporting an error in the construction of an std::fstream?

(I am tempted to ask "why the **** does the constructor not throw on failure, but let's not talk about that.)

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 1
    I'm not sure why you want constructor throw. afaict you can open the file after default construction. – apple apple Jul 26 '22 at 15:43
  • I'm pretty sure `is_open()` works the same as it did 14 years ago, and reliably reports a failure to open the file just like it always did. – Sam Varshavchik Jul 26 '22 at 15:47
  • 1
    @appleapple: Really? It is idiomatic in C++ for a constructor to result in a usable object in a valid state, and when this is not possible, for an error to be thrown. – einpoklum Jul 26 '22 at 15:47
  • @SamVarshavchik: `is_open()` gives me a boolean. I need to know what error has occurred. – einpoklum Jul 26 '22 at 15:48
  • @appleapple You could still use the object afterwards by opening a different file or using different flags so ig it'd be still in a usable state. – Staz Jul 26 '22 at 15:49
  • @einpoklum yes I didn't say it clearly, I mean I do know why you want it throw. But I don't think throw a exception is a good way here. – apple apple Jul 26 '22 at 15:49
  • @einpoklum I agree that it'd be nicer if the actual error was provided but you could manually do some checks using `std::filesytem` for file existence, permissions etc. – Staz Jul 26 '22 at 15:51
  • @Staz yes, though I'm not sure what part of my comment you're reply to – apple apple Jul 26 '22 at 15:53
  • 1
    @Staz: That can't work, in principle, since the filesystem state may change between my checks and the ifstream construction. – einpoklum Jul 26 '22 at 15:53
  • I'm pretty sure that "what error has occured" would get reported in `errno`. It is a safe bet that any error would be an OS-level error, reported in `errno`. – Sam Varshavchik Jul 26 '22 at 15:53
  • @SamVarshavchik: How sure is pretty sure? A 10-upvote answer to the old question says it's not always the case. – einpoklum Jul 26 '22 at 15:54
  • The old question is not asking about file open, but errors in general (only file open "for example"), and the answer is responding in general too. The original error, that set errno, might've occured, but other things have happened that set errno to something else. And I would say that that old answer is wrong. – Sam Varshavchik Jul 26 '22 at 15:56
  • @appleapple Sorry my bad, wrong tag. @einpoklum Fair enough, then ig `errno` would be the only way. – Staz Jul 26 '22 at 15:57
  • @Sam In what sense is it wrong? – Neil Butterworth Jul 26 '22 at 16:05
  • @SamVarshavchik in practice errno might always get set but I'm reasonably sure the standard offers no such guarantee, which I think is what the answer is trying to say – Alan Birtles Jul 26 '22 at 16:05

1 Answers1

1

Here's the best thing I can think of doing right now - "covering my ass" in case errno is somehow not set. At worst I'm wasting some cycles re-throwing in the "unhappy path".

// TODO: Consider checking errno here
std::filesystem::path file_path = whatever();
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
try {
    file.exceptions(std::ios::failbit | std::ios::badbit);
} catch (std::ios_base::failure& exception) {
    if (errno == 0) {
        throw;
    }
    throw std::system_error{ errno, std::generic_category(),
        "opening file " + file_path.native());
}
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Can file.exceptions throw? – Neil Butterworth Jul 26 '22 at 16:10
  • 1
    @NeilButterworth: That's what it's [intended to do](https://en.cppreference.com/w/cpp/io/basic_ios/exceptions) actually... – einpoklum Jul 26 '22 at 16:16
  • @Nathan No, I think you (and me) are wrong it will throw. – Neil Butterworth Jul 26 '22 at 16:20
  • @NeilButterworth Yep, just saw that. – NathanOliver Jul 26 '22 at 16:21
  • 1
    IIRC, a caught exception can't be re-thrown by name. IOW, `throw exception;` must be just `throw;` instead. Which means the code would have to look more like this instead: `catch (std::ios_base::failure& exception) { if (errno == 0) throw; throw std::system_error{ errno, ... }; }` or this: `catch (std::ios_base::failure& exception) { if (errno != 0) throw std::system_error{ errno, ... }; throw; }` It is a shame that C++ exceptions don't have a concept of "inner exceptions" like some other languages do, allowing the `failure` to be chained to the `system_error` or vice versa. – Remy Lebeau Jul 26 '22 at 18:30
  • Don't you need to `errno = 0` before the constructor? – KamilCuk Jul 27 '22 at 12:50
  • @KamilCuk: That depends on what you can assume about errno before the call. Added a comment. – einpoklum Jul 27 '22 at 12:54