2

When does an istreambuf_iterator throw an exception? I receive an exception when the underlying stream tries to read a directory, but not in other situations. Specifically, I grabbed the script readfile_tests.sh from Jan-Philip Gehrcke's blog and modified it slightly:

$ cat readfile_tests.sh 
#!/bin/bash

COMPILATION_SOURCE=$1
NE_FILE="na"
EMPTY_FILE="empty_file"
ONE_LINE_FILE="one_line_file"
INVALID_LINE_FILE="invalid_line_file"
FILE_READ="file_read"
FILE_WRITTEN="file_written"
FILE_DENIED="/root/.bashrc"
DIR="dir"

# compile test program, resulting in a.out executable
g++ -std=c++11 $COMPILATION_SOURCE

# create test files / directories and put them in the desired state
touch $EMPTY_FILE
if [[ ! -d $DIR ]]; then
    mkdir $DIR
fi
echo "rofl" > $ONE_LINE_FILE
echo -ne "validline\ninvalidline" > $INVALID_LINE_FILE
echo "i am opened to read from" > $FILE_READ
python -c 'import time; f = open("'$FILE_READ'"); time.sleep(4)' &
echo "i am opened to write to" > $FILE_WRITTEN
python -c 'import time; f = open("'$FILE_WRITTEN'", "a"); time.sleep(4)' & 

# execute test cases
echo "******** testing on non-existent file.."
./a.out $NE_FILE
echo
echo "******** testing on empty file.."
./a.out $EMPTY_FILE
echo
echo "******** testing on valid file with one line content"
./a.out $ONE_LINE_FILE
echo
echo "******** testing on a file with one valid and one invalid line"
./a.out $INVALID_LINE_FILE
echo
echo "******** testing on a file that is read by another process"
./a.out $FILE_READ
echo
echo "******** testing on a file that is written to by another process"
./a.out $FILE_WRITTEN
echo
echo "******** testing on a /root/.bashrc (access should be denied)"
./a.out $FILE_DENIED
echo
echo "******** testing on a directory"
./a.out $DIR

Then, I ran the tests on the following program

$  cat test.cpp
#include <iostream>
#include <vector>
#include <iterator>
#include <fstream>
#include <string>

int main(int argc, char* argv[]) {
    if(argc != 2) {
        std::cerr << "Provide one argument" << std::endl;
        return EXIT_FAILURE;
    }

    std::ifstream fin(argv[1]);
    std::istreambuf_iterator <char> eos;
    std::istreambuf_iterator <char> fin_it(fin.rdbuf());
    std::string str;
    try {
        std::copy(fin_it,eos,std::back_inserter(str));
    } catch(...) {
        perror("Error");
        throw;
    }
    std::cout << str;

    return EXIT_SUCCESS;
}

After running it, I received the following results:

$ ./readfile_tests.sh test.cpp
******** testing on non-existent file..

******** testing on empty file..

******** testing on valid file with one line content
rofl

******** testing on a file with one valid and one invalid line
validline
invalidline
******** testing on a file that is read by another process
i am opened to read from

******** testing on a file that is written to by another process
i am opened to write to

******** testing on a /root/.bashrc (access should be denied)

******** testing on a directory
Error: Is a directory
terminate called after throwing an instance of 'std::ios_base::failure'
  what():  basic_filebuf::underflow error reading the file
./readfile_tests.sh: line 51: 22563 Aborted                 ./a.out $DIR

These results are strange to me because an exception was thrown on the reading the directory test, but an exception was not thrown when trying to read /root/.bashrc. As such, when does istreambuf_iterator thrown an exception? In case it matters, I use gcc version 4.7.3.

Edit 1

Yes, opening a directory for reading is nonstandard and bad. At the same time, I need to trap this case correctly. Per the comments below, here is a piece of code that tries to read from a directory:

#include <iostream>
#include <vector>
#include <iterator>
#include <fstream>
#include <string>

void withIterators() {
    std::ifstream fin("mydirectory");
    if(!fin.is_open())
        perror("Error opening file");
    std::istreambuf_iterator <char> eos;
    std::istreambuf_iterator <char> fin_it(fin.rdbuf());
    std::string str;
    try {
        std::copy(fin_it,eos,std::back_inserter(str));
    } catch(...) {
        perror("Error reading file");
    }
}

void noIterators() {
    std::ifstream fin("mydirectory");
    if(!fin.is_open())
        perror("Error opening file");
    std::string line;
    while(getline(fin,line)) {}
    if(fin.bad())
        perror("Error reading file");
}

int main() {
    withIterators();
    noIterators();
}

After running, we receive the output:

Error reading file: Is a directory
Error reading file: Is a directory

This tells us two things. First, we can't catch the fact that we're opening a directory as a file after the constructor of fin. Second, the exception is thrown only when using iterators. The code noIterators above does not throw an exception when reading from a directory. Rather it sets the badbit. Why would the code throw an exception in one situation and set the badbit in the other?

Edit 2

I expanded the directory opening code from above in order to better track down what's going on

#include <iostream>
#include <vector>
#include <iterator>
#include <fstream>
#include <string>

void withIterators() {
    std::ifstream fin("mydirectory");
    if(!fin.is_open())
        perror("Error opening file");
    std::istreambuf_iterator <char> eos;
    std::istreambuf_iterator <char> fin_it(fin.rdbuf());
    std::string str;
    try {
        std::copy(fin_it,eos,std::back_inserter(str));
    } catch(...) {
        perror("Error reading file");
    }
}

void withIteratorsUnderflow() {
    std::ifstream fin("mydirectory");
    try {
        fin.rdbuf()->sgetc();
    } catch(...) {
        perror("Error reading file");
    }
}

void withOtherIterators() {
    std::ifstream fin("mydirectory");
    if(!fin.is_open())
        perror("Error opening file");
    std::istream_iterator <char> eos;
    try {
        std::istream_iterator <char> fin_it(fin);
    } catch(...) {
        perror("Error making iterator");
    }
}

void noIterators() {
    std::ifstream fin("mydirectory");
    if(!fin.is_open())
        perror("Error opening file");
    std::string line;
    while(getline(fin,line)) {}
    if(fin.bad())
        perror("Error reading file");
}

int main() {
    withIterators();
    withIteratorsUnderflow();
    withOtherIterators();
    noIterators();
}

Basically, we get exceptions, or not, in different places, but for similar reasons.

On withIterators(), the call to copy eventually calls the code /usr/lib/gcc/i686-pc-linux-gnu/4.7.3/include/g++-v4/bits/streambuf_iterator.h:187, which states

else if (!traits_type::eq_int_type((__ret = _M_sbuf->sgetc())

The call to sgetc() throws the exception. Likely, this is calling underflow as @user657267 suggested.

On withIteratorsUnderflow(), we see the exception get thrown directly by sgetc, which confirms what happens with withIterators().

On withOtherIterators(), we have the exception thrown as soon as we create the std::istream_iterator. This differs from what happens with std::istreambuf_iterator, which didn't throw the exception until later. In any case, during construction, we call the code /usr/lib/gcc/i686-pc-linux-gnu/4.7.3/include/g++-v4/bits/stream_iterator.h:121, which states

*_M_stream >> _M_value;

Basically, the class istream_iterator creates an istream on construction that immediately tries to read a character, which throws the exception.

On noIterators(), getline just sets the badbit and does not throw the exception.

The bottom line is that trying to read from a directory is bad. Is there a good stl (not boost) way to detect this? Are there other situations that will throw an implementation specific exception that will need to be trapped?

wyer33
  • 6,060
  • 4
  • 23
  • 53

1 Answers1

1

istreambuf_iterator itself will never throw.

The basic_filebuf it iterates over opens the file as if by calling std::fopen (whether it actually does so or not is irrelevant).

As you can see in this question, standard C has no concept of what a directory is, so attempting to open one is either implementation defined or undefined behaviour. In gcc's case (or rather libc) the directory can be opened but you can't really do much with it.

According to the standard, a basic_filebuf will also never throw, but since you've already gone beyond what the standard enforces by attempting to open something that isn't a file, libstdc++ is free to throw an exception when you try to read from the dir.

Checking whether a given name is a file or not is usually platform dependant, but boost::filesystem / std::experimental::filesystem offer portable checks.


To answer your edit, you won't see the exception when attempting to read from the streams because the sentry will fail and the stream will never read from the buffer (which ultimately calls underflow). istreambuf_iterator operates directly on the buffer so there's no sentry.

It also turns out that libstdc++ won't throw on a similar call to overflow, and due to the way underflow is set up the exception won't be thrown if the stream is open for writing and overflow returns eof.

Community
  • 1
  • 1
user657267
  • 20,568
  • 5
  • 58
  • 77
  • @BЈовић The libstdc++ implementation of `underflow` returns early if the stream was opened for writing and `overflow` returns eof. Funnily enough `overflow` won't throw a similar exception for directories. It's all a bit quirky, I guess the moral of the story is: don't open directories. – user657267 Oct 07 '14 at 10:10