1

I have C++ class Question to hold data from a file questions.txt of multiple choice questions and answers:

update: I have updated the &operator>> operator overload I have one:

  1. it only insert first multiple choice question of 2 multiple choice questions"read the first Question "

Data in file questions.txt:

A programming language is used in this Course? 3

1. C
2. Pascal
3. C++
4. Assembly

What compiler can you use to compile the programs in this Course? 4

1. Dev-C++
2. Borland C++Builder
3. Microsoft Visual C++
4. All of the above

I'm trying to insert the multiple answers into a map. I just want to ask how to overload operator>> to iterate over multiple answers to insert them into a map:

#include <string>
#include <iostream>
#include <sstream>
#include <map>
using namespace std;
class Question
{
    string question;
    int correctIndex;
    map<int,string> answers;


    friend std::istream &operator>>(std::istream &is, Question &q) {
        getline(is, q.question, '?');   // stops at '?'
        is>> q.correctIndex;
        string line;
        while (getline(is, line) && line.empty())  // skip leading blank lines
            ;
        while (getline(is,line) && !line.empty())  // read until blank line
        {
            int id;
            string ans;
            char pt;
            stringstream sst(line);    // parse the line;
            sst>>id>>pt;               // take number and the following point
            if (!sst  || id==0 || pt!='.')
                cout << "parsing error on: "<<line<<endl;
            else {
                getline (sst, ans);
                q.answers[id] = ans;
            }
        }
       return is;
    }
};

int main()
{
    ifstream readFile("questions.txt");//file stream
    vector<Question> questions((istream_iterator<Question>(readFile)), istream_iterator<Question>());
}
NinjaDeveloper
  • 1,620
  • 3
  • 19
  • 51

2 Answers2

2

There are two issues with your code: skipping the first answer and reading through the end of the file.

In this pair of loops:

while (getline(is, line) && line.empty())  // skip leading blank lines
    ;
while (getline(is,line) && !line.empty())  // read until blank line
{

the first non-empty line will terminate the first loop, but then immediately you call getline() again without actually reading any of its contents. This skips the first answers choice. You'll want to make sure that you don't actually call getline() the first time. Something like...

// skip leading blank lines
while (getline(is, line) && line.empty()) {
    ;
}
for (; is && !line.empty(); getline(is, line)) {
    // ...
}

But the second and bigger problem is if you read through the end of the file (as your code does right now) the last operator>> will cause the istream to eof(), which will disregard the last Question that you have streamed. This is tricky since you have a variable-length input stream - we don't know when we've run out of input until we've actually run out of input.

Thankfully, we can do everything quite a bit simpler. First, instead of reading off the end of the input to trigger the error, we'll use the first read to cause us to stop:

friend std::istream &operator>>(std::istream &is, Question &q) {
    if (!getline(is, q.question, '?')) {   // stops at '?'
        return is; 
    }

This way, if we hit EOF early, we stop early. For the rest, we can simply the reading greatly by using skipws(). Instead of manually looping through the empty lines (which is hard to do right, as per your initial bug), we can let operator>> do this for us by just skipping ahead.

When we run out of things to read, we just back out of the error flags - since we don't want fail() (if we try to read the next index and it's actually the next question) or eof() (we're done) triggered.

Altogether:

friend std::istream &operator>>(std::istream &is, Question &q) {
    if (!getline(is, q.question, '?')) {   // stops at '?'
        return is; 
    }

    is >> q.correctIndex;

    int id; 
    char pt; 
    string ans;

    is >> skipws;
    while (is >> id >> pt && getline(is, ans)) {
        q.answers[id] = ans;
    }

    // keep the bad bit, clear the rest
    is.clear(is.rdstate() & ios::badbit);
    return is; 
}   

Now that's also a little incomplete. Perhaps you want to indicate error if you don't read into answers anything that matched correctIndex? In that case, you would set the ios::failbit too.

Barry
  • 286,269
  • 29
  • 621
  • 977
0

First improvement

When the operator>> is used for a string, it stops at the first blank separator. So for reading correctly the question you should consider:

friend std::istream &operator>>(std::istream &is, Question &q) {
    getline(is, q.question, '?');   // stops at '?'
    return is>> q.correctIndex;
    ... // to do:  for the answers (see below)
}

You could consider a similar approach, for reading each question, starting with its id. Unfortunately, using operator>> on int will not allow us to detect the last answer: the reading attempt would fail with the start of a non-numeric text for the next question.

The problem with the format

The format that you use has some ambiguities:

  1. Are blank lines mandatory and mark the begin and end of the answers ? In this case the last question is invalid : an end of answer is missing).
  2. Or are the blank lines optional and have to be ignored ? In this case, the first char determines if it's the start of a new question (non numeric) or if it's a new answer (numeric)
  3. Or is it always expected that there are exactly 4 answers for a question ?

Alternative 1: a blank line marks end of question

The idea is to read line by line and parsing each line separately:

    ...    
    string line; 
    while (getline(is, line) && line.empty())  // skip leading blank lines
       ;  
    do                                        // read until blank line 
    {
        int id;
        string ans; 
        char pt; 
        streamstring sst(line);    // parse the line;
        sst>>id>>pt;               // take number and the following point
        if (!sst  || id==0 || pt!='.') 
           cout << "parsing error on: "<<line<<endl; 
        else {
           getline (sst, ans); 
           q.answers[id] = ans; 
        }
        getline(is,line);
    } while (getline(is, line) && !line.empty());

Attention: as per hypothesis: the missing end-of-answer blank line, will cause the reading of the last question to fail. Ideally, you'd issue an error message to clarify (e.g. unexpected end of file). Correcting the input file with an empty blank line will work (an empty line ended with a new line).

Alternative 2: test first char of line to see if it's still next answer

The other alternative peeks the first character to read in order to check if it is an answer (starts with a digit), an empty line (to be skipped) and if not, it exits the loop.

    ...    
    string line, ans;
    int c, id; 
    char pt; 
    while ((c = is.peek())!=EOF && (isdigit(c) || c=='\n')) {  // test first char without reading it
        getline(is, line); 
        if (!line.empty()) {
            stringstream sst(line);  
            ...    // parse the line as above
            }
        }
    }

With this option, the requirement is that the answers ends with a newline (i.e. trailing '\n'). An unfinished line interrupted with an EOF will cause the last question to be ignored as failed.

Christophe
  • 68,716
  • 7
  • 72
  • 138
  • Hi @Christophe I have update issues can you take a look ? – NinjaDeveloper Nov 13 '16 at 18:48
  • @NinjaDeveloper Oops sorry, I was a little quick: the first loop ends either if EOF was reached of the line is not empty. In the latter case (e.g. your answer "1. C") you need to process line first, before getting a new one. I've updated accordingly. – Christophe Nov 13 '16 at 19:54