1

This code accepts student name, father's name, roll no and age from input file and put it in a presentable manner in output file.

In this code, when contents of input file are:

Vicky
Mohan
20094567
22   Ricky
Rahul
20091234
21

It works fine.

But if they are:

Vicky
Mohan
20094567
22
Ricky
Rahul
20091234
21

It enters into an infinite loop. Any suggestions??

ifstream inps("input", ios::in);
outs.open("output",ios::app);

string line;
int data,count=1;

for(getline(inps,line);line!="";getline(inps,line))
{
    count++;

    s1.setName(line);
    getline(inps,line);
    s1.setFatherName(line);
    inps >> data;
    s1.setRollNo(data);
    inps >> data;
    s1.setAge(data);

    outs.open("output",ios::app);
    outs << "Student name: " << s1.getName() << endl;
    outs << "Father’s name: " << s1.getFatherName() << endl;

    outs << "Roll number: " << s1.getRollNo() << endl;
    outs << "Age: " << s1.getAge() << endl << endl;
}

inps.close();
outs.close();

3 Answers3

6

It's because of how you read the input. You never actually check if it succeeds or not.

You need to do e.g.

while (std::getline(...))
{
    ...
}
Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • This is fine under the assumption the input is well-formed, since there are more reads inside the loop body. However, the original code also makes that assumption. – R. Martinho Fernandes Aug 06 '13 at 12:03
  • @R.MartinhoFernandes At least it will not enter an infinite loop. – Some programmer dude Aug 06 '13 at 12:06
  • 1
    @JoachimPileborg It won't go into an infinite loop, but it will still leave the cases of undefined behavior inside the loop. And `std::getline` _won't_ erase the string in her case, because the erasure doesn't occur until _after_ the construction of the sentry object, and only if the sentry object tests OK. (Which it won't, given her code.) – James Kanze Aug 06 '13 at 12:56
5

The reason for the symptoms you describe is that you're mixing formatted input with getline. There's also a fundamental problem that you never check whether any of the input succeeds.

The real problem manifests itself after the inps >> data lines: these lines skip whitespace and read an int, and no more. In particular, they leave any trailing whitespace, including the '\n' character, in the stream. So in your second case of input, after reading 22, there is still a '\n' in the stream, which will terminate the next call to getline (which instead of reading " Ricky", will read ""). This causes the input to become unsynchronized, which shortly results in your doing inps >> data when the stream is positionned at "Rahul". Trying to read an int when the input is "Rahul" fails, and failure is sticky; it will remain until you reset it, and all further attempts are no-ops. Since you've already read something into line once, it won't ever become empty, and you loop forever, doing nothing.

The first, and most important change is to check after every input that the input succeeded, and not try to read further if it hasn't. (The structure of your file is such that you probably can't reliably resynchronize if there is an error. Otherwise, it is a good policy to try and resynchronized, and continue, so that you can catch multiple errors in the input.)

The second thing you need to do is ensure that you read a complete line (including the '\n') when inputting integers. There are two ways of doing this: the classic way is to use getline, then initialize an std::istringstream with the line, and input the int using this. (This allows additional error checking, e.g. that there is no additional garbage in the line.) Alternatively, you can call inps.ignore( std::numeric_limits<std::streamsize>::max(), '\n' );, which will extract and ignore characters until '\n' (which is also extracted).

EDIT:

On rereading, it occurs to me that my text description isn't all that clear, so here's what happens in a step-wise explination:

  • The first time through the loop, everything works as expected, but the input position is immediately behind the "22" (which was the last input).

  • The getline at the top of the loop is called. It will return all of the characters between the "22" and the end of that line. If the "22" is immediately followed by a new line, this should result in an empty line, terminating the loop (although there is still more data to be read). If there are extra characters after the "22" (say a blank or so), then these will be read as the line.

  • Assuming there were extra characters, you then read " Ricky" as the father's name, and do inps >> data for the roll number on the string "Rahul". This fails, and sets the stream in an error condition, which causes all further operations to be no-ops.

  • So when you next reach the top of the loop, the getline is a no-op, the previous contents of line are unchanged, and you enter the loop again. And again, and again, because until you clear the error, all operations will be no-ops. All of the variables hold their old values.

The simplest solution is probably that suggested by Neil Kirk in a comment: read the entire file into an std::vector of lines, and parse those:

class Line
{
    std::string myContents;
public
    friend std::istream& operator>>( std::istream& source, Line& obj )
    {
        std::getline( source, obj.myContents );
        return source;
    }
    operator std::string() const { return myContents; }
};

// ...
std::vector<Line> lines( (std::istream_iterator<Line>( inps )),
                         (std::istream_iterator<Line>()) );

If you want to read the file on the fly, however (say because it might be too big to fit into memory, or simply because it is a good learning exercise):

while ( std::getline( inps, line ) && !line.empty() ) {
            //  but do you really what the second condition.
            //  if so, you should probably provide
            //  a function which will ignore whitespace.
    s1.setName( line );
    if ( std::getline( inps, line ) ) {
        s1.setFatherName( line );
    }
    if ( std::getline( inps, line ) ) {
        std::istringstream s( line );
        int data;
        if ( s >> data ) {
            s1.setRollNo( data );
        }
    }
    if ( std::getline( inps, line ) ) {
        std::istringstream s( line );
        int data;
        if ( s >> data ) {
            s1.setAge( data );
        }
    }
}

This is very succinct. It still needs additional error checking, and you probably want to keep track of the line number so that you can output it with any error message. But it shoul point you in the right direction.

EDIT2:

Also, you don't want to open the output file each time through the loop. Attempting to open an already open std::ofstream will fail, an as above, once the stream has failed, all further attempts to use it are no-ops.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
2

Replace

for(getline(inps,line);line!="";getline(inps,line))

with

while (getline(inps, line))
cpp
  • 3,743
  • 3
  • 24
  • 38
  • That loses one condition. Make it `while (getline(inps, line) && !line.empty())` – R. Martinho Fernandes Aug 06 '13 at 12:05
  • 1
    Or `for (string line; getline(inps, line) && !line.empty();)` to limit string variable to loop scope. – Felix Glas Aug 06 '13 at 12:06
  • @Snps There's really no need to limit the scope here. – James Kanze Aug 06 '13 at 12:53
  • And while this _will_ get rid of the endless loop, it still won't result in correct code (and will still leave formally undefined behavior inside the loop). – James Kanze Aug 06 '13 at 12:54
  • @JamesKanze Agreed, but still he's not using the variable outside the loop. Limiting scope of variables as much as possible is a good coding convention IMO. – Felix Glas Aug 06 '13 at 13:37
  • @Snps Why? It's _generally_ a good idea to not define a variable before it can be correctly initialized (but that's often not possible where input is involved). And of course, you don't want a global or a static where non-static local will do the job. But once the variable is local, what advantage does limiting its scope further bring you? – James Kanze Aug 06 '13 at 14:27
  • @JamesKanze Encapsulation. I *generally* don't want to leak implementation outside of scope. Not limiting the variable inside scope makes the behaviour inside the scope to depend on the value of the variable before entering the scope, *generally*. – Felix Glas Aug 06 '13 at 14:54
  • @Snps Implementation granularity is the function. And your second phrase corresponds to what I said: delay declaration until you can properly initialize, _whenever possible_. (In the case of initialization by `std::getline`, it isn't possible.) But this has nothing to do with scope. – James Kanze Aug 06 '13 at 15:03
  • @JamesKanze I think implementation granularity can be individual parts of the function (as for loops) if it helps readability. And if we flip the pancake, why shouldn't I limit the variable inside scope? – Felix Glas Aug 06 '13 at 15:12
  • @Snps If the function contains a loop, the function is the loop. And it's perfectly fine to declare the argument inside the loop; if you want a clean, new copy each time through, it's a requirement. But declaring the variable in the initialization part of the for, when it has nothing to do with the `for` otherwise, is not defining the variable inside the loop. It's added verbosity for nothing, _and_ it's added confusion for the reader, because you don't really have a for loop. – James Kanze Aug 06 '13 at 15:31
  • @JamesKanze I don't really understand what you mean with your first phrase. The advantage of declaring a variable in the init part of a `for` is that the variable will not exist before the statement, neither after exiting the loop, where it is used. I give you that using a `for` loop in this way is verbose for the purpose of the `for`, but the principle of encapsulation can be applied using it. Also it modularises that specific block of code which IMO translates to readability. – Felix Glas Aug 06 '13 at 15:55
  • @Snps The advantage of defining a variable in the loop is that it is constructed new every time you pass through the loop. That doesn't hold for a variable in the init clause of the for. If you have no need for the variable elsewhere, and it is used to manage the for, then the init clause of the for is great. Replacing a while with a for, in order to use the init clause for something else, is confusing. – James Kanze Aug 06 '13 at 17:19
  • @JamesKanze Well it's not *that* confusing and I think my previous point still holds in many cases. Code as `bool flag = true; while(flag) {/* ... */}` is troublesome as it requires the programmer to treat both statements as one, even if the compiler does not. In *principle* it's the opposite of abstraction and requires more effort from the programmer. – Felix Glas Aug 06 '13 at 18:07
  • @Snps But that has nothing to do with the question. _If_ the variable is used to control the loop, then it makes perfect sense to define it in the init part of the `for` statement. Given a `while` loop and a totally unrelated variable just before it, it doesn't. – James Kanze Aug 06 '13 at 18:22
  • @JamesKanze I get your point, even though for me this was really more about code convention in the general case. I also *do* think that the variable is related as the `while` loop depends on that variable. If the programmer would e.g. move `line` into another `std::string` in between declaration of the variable and the loop this would result in UB. – Felix Glas Aug 06 '13 at 19:10