Search code examples
c++iobuffereoffile-descriptor

How would one generalise `clearerr()` under C++?…


TL;DR

I am aware that if a program listens for EOF (e.g. ^D) as a sign to stop taking input, e.g. by relying on a conditional like while (std::cin) {...}, one needs to call cin.clear() before standard input can be read from again (readers who'd like to know more, see this table).

I recently learned that this is insufficient, and that the underlying C file descriptors, including stdin, need clearerr() to be run to forget EOF states.

Since clearerr() needs a C-style file descriptor, and C++ operates mainly with std::basic_streambufs and the like (e.g. cin), I want to generalise some code (see below) to run clearerr() on any streambuf's associated C-style file-descriptor, even if that may not be stdin.

EDITS (1&2):
I wonder if stdin is the only ever file-descriptor that behaves like this (needing clearerr() to run) ...?
If it isn't, then the following code should end the question of generalisation (idea pointed out by zkoza in their answer)

As zkoza pointed out in their comment below, stdin is the only file-descriptor that would, logically, ever need such treatment (i.e. clearerr()). Checking whether a given C++ stream is actually really attached to *std::cin.rdbuf() is all that is needed:

std::istream theStream /* some stream with some underlying streambuf */

if (theStream.rdbuf() == std::cin.rdbuf())
    clearerr(stdin);

Background

I'm writing a tool in C++ where I need to get multiple lines of user input, twice.

I know there are multiple ways of getting multiline input (e.g. waiting for double-newlines), but I want to use EOF as the user's signal that they're done — not unlike when you gpg -s or -e.

After much consultation (here, here, and on cppreference.com), I decided to use... (and I quote the third):

[the] idiomatic C++ input loops such as [...]

while(std::getline(stream, string)){...}

Since these rely on std::basic_ios::operator bool to do their job, I ensured that cin.rdstate() was cleared between the first and second user-input instructions (using cin.clear()).

The gist of my code is as follows:

std::istream& getlines (std::basic_istream<char> &theStream,
                        std::vector<std::string> &stack) {
    std::ios::iostate current_mask (theStream.exceptions());
    theStream.exceptions(std::ios::badbit);
    std::string &_temp (*new std::string);
    while (theStream) {
        if (std::getline(theStream, _temp))
            stack.push_back(_temp);   // I'd really like the input broken...
                                      // ... into a stack of `\n`-terminated...
                                      // ... strings each time
    }

    // If `eofbit` is set, clear it
    // ... since std::basic_istream::operator bool needs `goodbit`
    if (theStream.eof())
        theStream.clear(theStream.rdstate()
                        & (std::ios::failbit | std::ios::badbit));
                        // Here the logical AND with
                        // ... (failbit OR badbit) unsets eofbit
    
    // std::getline sets failbit if nothing was extracted
    if (theStream.fail() && !stack.size()) {
        throw std::ios::failure("No input recieved!");
    }
    else if (theStream.fail() && stack.size()) {
        theStream.clear(theStream.rdstate() & std::ios::badbit);
        clearerr(stdin); // 👈 the part which I want to generalise
    }
    
    delete &_temp;
    theStream.exceptions(current_mask);
    return theStream;
}

Solution

  • This does what you need:

    #include <iostream>
    
    int main()
    {
        std::cin.sync_with_stdio(true);
        char c = '1', d = '1';
        std::cout << "Enter a char: \n";
        std::cin >> c;
        std::cout << (int)c << "\n";
        std::cout << std::cin.eof() << "\n";
        std::cin.clear();
        clearerr(stdin);
        std::cout << std::cin.eof() << "\n";
    
        std::cout << "Enter another char: \n";
        std::cin >> d;
        std::cout << (int)d << "\n";
        std::cout << std::cin.eof() << "\n";
    }
    

    It works because C++'s std::cin is tied, by default, with C's stdin (so, the first line is actually not needed). You have to modify your code to check if the stream is std::cin and if so, perform clearerr(stdin);

    EDIT:

    Actually, sync_with_stdio ensures only synchronization between the C and C++ interfaces, but internally they work on the same file descriptors and this may be why clearerr(stdin); works whether or not the interfaces are tied by sync_with_stdio

    EDIT2: Does these answer your problem? Getting a FILE* from a std::fstream https://www.ginac.de/~kreckel/fileno/ ?