tags:

views:

298

answers:

2

In C++, you can use non-member getline() with a stream in a loop like this:

#include <string>
#include <fstream>
#include <cstdlib>
using namespace std;

int main() {
    ifstream in("file.txt");
    if (!in) {
        return EXIT_FAILURE;
    }
    for (string line; getline(in, line); ) {
        // Do stuff with each line
    }
}

However, I want to do that with a FILE* created by _wfopen("file.txt", "r") instead, so I created one:

#include <cstdio>
#include <string>
#include <cstdlib>
#include <cwchar>
using namespace std;

bool getline(FILE* const in, string& s) {
    int c = fgetc(in);
    if (c == EOF) {
        return false;
    }
    s.clear();
    while (c != EOF && c != 10 && c != 13) {
        s += c;
        c = fgetc(in);
    }
    return true;
}

int main() {
    FILE* const in = _wfopen(L"file.txt", L"r");
    if (!in) {
        return EXIT_FAILURE;
    }
    for (string line; getline(in, line); ) {
        // Do stuff with the line
    }
    if (in) {
        fclose(in);
    }
}

It handles newlines like I want and works in a loop like I want. It's just too slow because I'm reading one char at a time and inserting one char in the string at a time. For example, it takes 6 seconds to process a 12MB file while the original getline does it virtually instantly. That's not that big of a deal for a small file, but for a 2GB file for example, it'd be a problem.

I'd like it to be as fast as C++'s getline(), but I don't think I can make it any faster without redesigning it.

So, how should I redesign it so it's more efficient?

I know I should fread in chunks into a buffer (a vector for example and resize when needed) till I find() a newline or newline pair in it and append the range to the string. However, I'm not really picturing how to make it work like my char-by-char version, especially if I read in too much and have to put data after the newline or newline pair back into the stream so it can be consumed on the next iteration.

Now, VC++ has a wifstream that takes a FILE* and STLPort might have one too. But, I'm using just Mingw 4.4.1. (I don't want to use STLPort because it's a pain in the ass to build with Mingw.)

The reason I need to use a FILE* is because that's what _wfopen() returns. I need to use _wfopen() because it supports wchar_t* paths that I will be getting from the wchar_t** array returned by windows function CommandLineToArgvW(CommandLineW(), &argc). ifstream doesn't take a wide path.

Thanks

A: 

It's possible that your std::string implementation does not grow strings in a way that's efficient for appending many characters one-by-one. One thing to try might be to use std::string::reserve() to double the string capacity when the buffer is full.

Edit: BTW, I should add that if you're expecting to open the FILE* in text mode, you do not need to check against both \n and \r, as newline conversion appropriate for the platform is performed automatically by the C stdio functions in text mode. (If, however, you intend to read files created on other platforms (e.g. reading Windows files on Unix), then you would need to check for the various types of line endings.)

jamesdlin
Yes, opening in text mode. However, I just wanted newline handling right if opening in binary just for good measure. But, that's no biggy. Thanks
Shadow2531
+2  A: 

You should be using C++ I/O facilities if you're programming in C++. Having said that...

First, you are checking for newline by checking for 10 and 13. You should open your file in text mode, and check for '\n' instead. This method is portable, and works with different line-end conventions, as well as on non-ASCII systems.

Assuming you have to use native C FILE *, I would do it this way:

#include <cstdio>
#include <cstring>
#include <string>

bool cgetline(FILE* const in, std::string &s)
{
    char buf[BUFSIZ+1] = {0};
    s.clear();
    while (fgets(buf, sizeof buf, in) != NULL) {
        char *end = strchr(buf, '\n');
        if (end == NULL) {
            /* We didn't see a newline at the end of the line,
               if we hit the end of file, then the last line wasn't terminated
               with a newline character.  Return it anyway. */
            if (feof(in)) {
                s.append(buf, strlen(buf));
                return true;
            } else {
                s.append(buf, sizeof buf - 1);
            }
        } else {
            s.append(buf, end - buf);
            return true;
        }
    }
    return false;
}

The complication is from making sure the program does the right thing when the last line of a file doesn't end with a newline character.

Reading from a file character-by-character and appending to a string is probably why your version is slow.

Alok
There are subtle bugs in your implementation, which is normally why fgets is avoided like the plague. If the last line of a file doesn't have a '\n' at the end of it, the fgets return will not either, but that doesn't meant the whole buffer is valid. (http://www.rocketaware.com/man/man3/fgets.3.htm)
Todd Gardner
In C, `fgets()` is one of the *safer* input routines. As I said, the OP really wants a "C" solution. I hope I have eliminated most of the bugs now (except the no-newline-at-end bug).
Alok
@Todd, thanks for your comment. I believe I have fixed the bug with no-newline-at-end files, but of course, I don't claim complete correctness even now! :-)
Alok
@AloK, your implementation has the performance I want and with your additional fix, it works with the last line too. Everything seems to work great, but I want to test for a while and see what other solutions are provided first. Thanks
Shadow2531
@Shadow: I haven't tested my code fully myself - it was written quickly for illustration more than anything. Todd's comment helped me improve it, but as you said, you should test it ("I haven't proven it correct, nor tested it (fully)!").
Alok
It does have one more bug (which is the other problem listed in that manpage), embedded NULL in the file (strbuf would return NULL and feof would return false, even though the buffer might not be filled)
Todd Gardner
@Alok, understood. Your code alone helps me picture how to do things. That helps big time. Will probably give you accepted answer. Just don't want to jump the gun on that just yet.
Shadow2531
@Todd, can a text file have embedded `\0`s? I would argue that any file with a 0 in it is not a text file anymore.
Alok
@Alok, the text files I'd be reading wouldn't (well shouldn't) have NULLs in them.
Shadow2531
@Alok, your example does exaclty what I asked for. Accepting.
Shadow2531
I checked STLPort's non-member getline implementation (in _string_io.c). It reads char-by-char and appends to the string char-by-char. But, it's fast because it grabs each char from the stream's streambuf (returned by .rdbuf()) (via sbumpc()). It'd be cool to do something like that with FILE*.
Shadow2531