seekg doesn't go to beginning of file

Viewed 82

I'm trying to make a random name generator. The problem is that in order to get the count of lines in the file, I have to loop through it.

So when I need to loop through it again in getRandomName() to get a name, it has already reached the end of the file

I tried solving the issue with seekg(0, std::ios::beg) but it doesn't work for some reason.

int getLineCount(std::fstream &names) {
  int count{};
  while (names) {
    std::string name;
    getline(names, name);
    ++count;
  };
  // last line is empty
  return count - 1;
}

std::string getRandomName(std::fstream &names, int lineCount) {
  int randomNum{getRandomNumber(1, lineCount)};
  std::string name;
  names.seekg(1, std::ios::beg); // here i try to go to the beginning but it doesnt work
  for (int i{0}; i < randomNum; ++i) {
    names >> name;
  };
  return name;
};

int main() {
  std::srand(static_cast<unsigned int>(std::time(nullptr)));
  std::rand();
  std::fstream names{"names.txt"};

  int lineCount{getLineCount(names)};
  std::cout << getRandomName(names, lineCount);
}
4 Answers

The problem is that a file stream considers reaching the end of the file as an error condition and sets the according bits, both the fail-bit and the EOF-bit. As long as this state persists, any further file operations fail. You can set the stream back to the normal operating state by clearing the error state, though – if you do so, then you'll be able to proceed as intended.

If need those lookups frequently then it might be worth to consider buffering the data lines within a std::vector<std::string> – unless if you have to handle extremely large data (thus provoking paging effects) this would be far more efficient. Even with paging effects, but with large enough disk space available you still get better for every lookup as you'd have to load at most one memory page back from disk.

If you need the lookup just once then you might get along without the getLineCount function entirely – select a random value from entire maximum range and just count the number of lines until you found the desired line – or the end of file got reached. If the latter happens, then recalculate the random index based upon the number of lines found and iterate of over the file again. The larger your file is, the greater is the chance that you only need to iterate once, and if you still need to do twice, nothing is lost anyway... Note, though, that this approach requires your random number generator generating equally distributed random numbers!

This would work for multiple calls as well, though the chance of a benefit get's smaller as with every further call the chance of reading beyond file size at least once increases.

Your getLineCount() functions reads with getline() through the file until nothing can be read anymore. When it arrives at the end, an error state is set, with eofbit.

All subsequent actions on the stream will fail, including seekg(0, std::ios::beg);, until you names.clear(); the error state.

By the way, looping on getline() avoids getline() to fail in the loop body, and makes the -1 unnecessary. Another thing you could do is to make your function neutral for the read position of the file. It's optional bu would me more consistent with the name of your function which suggests that it just gets something, not that it consumes the stream to the end.

int getLineCount(std::fstream &names) {
    int count{};
    std::string name;
    auto old_pos = names.tellg();   // backup current position
    while (getline(names, name)) 
        ++count;
    names.clear();                  // reset eof error caused by loop
    names.seekg (old_pos, std::ios::beg);  // restore position        
    return count;
}

Not related

Your random position might lead to inconsistencies, if names on a line can include whitespaces, because >> reads space separated strings and not full lines. E.g. if your file has two lines:

 Bjarne Stroustrup
 B.W.Kernighan

Your random read could return Bjarne or Stroustrup but never B.W.Kernighan because there are 2 lines but 3 space separated strings. So better read the random line as you count them, using getline() again.

Your first step should start from adding logs (I've also fixed minor issues, like inconsistent reading of data, 1 instead 0 and so on).

#define LOG(x) std::cerr << __LINE__ << " " #x " = "<< x << '\n'

int getRandomNumber(int a, int b)
{
    static std::random_device rd; 
    static std::mt19937 gen(rd());
    std::uniform_int_distribution<int> distrib(a, b);

    return distrib(gen);
}

int getLineCount(std::istream &names) {
  int count{};
  std::string name;
  while (getline(names, name)) {
    ++count;
    LOG(count);
    LOG(names.tellg());
  };
  return count - name.empty();
}

std::string getRandomName(std::istream &names, int lineCount) {
  int randomNum{getRandomNumber(1, lineCount)};
  LOG(randomNum);
  std::string name;
  LOG(names.tellg());
  names.seekg(0, std::ios::beg);
  LOG(names.tellg());
  for (int i{0}; i < randomNum; ++i) {
    getline(names, name);
  };
  return name;
};

int main() {
  std::ifstream names{"names.txt"};
  LOG(names.tellg());

  int lineCount{getLineCount(names)};
  LOG(lineCount);
  std::cout << getRandomName(names, lineCount);
}

This produces this output https://wandbox.org/permlink/EPLHqMBg1s8Awe9F :

44 names.tellg() = 0
23 count = 1
24 names.tellg() = 13
23 count = 2
24 names.tellg() = 27
23 count = 3
24 names.tellg() = 40
23 count = 4
24 names.tellg() = 53
23 count = 5
24 names.tellg() = -1
47 lineCount = 5
31 randomNum = 4
33 names.tellg() = -1
35 names.tellg() = -1

-1 indicates that stream is in error state.

And this is obvious you have read file to the end, so there was attempt to read beound file and error flag is set.

When error flag is set, stream is unusable until flag is cleared. So just adding names.clear(); in proper place fixes issue: https://wandbox.org/permlink/Us6b3Jw3v6JFwlpX

In your getLineCount() function use while (names.peek() != EOF) instead.

Related