Monday, October 29, 2012

File wrapper class in C++ (2)

Last time I showed how to implement a File class. In the solution Brian (pictured on the right) and me used a second class as a wrapper around a standard FILE pointer. The two classes worked together by means of a shared_ptr. The shared_ptr ensured that the file would not be closed until there were no more references to the file wrapper object. Reread the last post to see what I'm talking about.

A reader of this blog pointed out that there is another solution, one that turns out far more idiomatic. The constructor of shared_ptr accepts an instance of a functor, named a Deleter. This functor will be called when the reference count drops to zero, and the object will be destructed. The resulting code looks like this:
#include <cstdio>
#include <tr1/memory>

class File {
public:

    // conversion constructor
    File(FILE *f) : stream_(std::tr1::shared_ptr<FILE>(f,
        FileDeleter() )) { }

    // copy constructor copies the shared_ptr
    File(const File& f) : stream_(f.stream_) { }

private:
    std::tr1::shared_ptr<FILE> stream_;

    // local functor closes the FILE upon destruction   
    class FileDeleter {
    public:
        void operator()(FILE *f) const {
            if (f != NULL)
                std::fclose(f);
        }
    };
};
Right here I inlined the FileDeleter class. You may also choose to put it outside the enclosing class, or it can even be implemented as a struct with operator()().

All in all, this solution is more elegant and more idiomatic than the previous one.

Saturday, October 13, 2012

File wrapper class in C++

Last week my good friend Brian (see picture on the right) and I encountered an interesting problem: wrapping up a file descriptor in a File class. Making class wrappers in C++ generally isn't all that hard, but this particular one was a lot more tricky than we had expected.

What we wanted to have was a File class that would act the following way:
  • based on standard C's FILE*
  • factory function open() creates a File, and opens it
  • destructor of File closes the open file
So that you could write code like this:
    File f = open("filename");
    f.write("hello\n");

    // destructor closes the file
You might code open() like this:
File open(const char *filename) {
    File f;
    f.open(filename);
    return f;
}
Unfortunately, this code doesn't work, as it has a bug. Do you see it already?
The thing is, when you return f, the value f gets copied [by means of the copy constructor] and then the destructor gets called in the original f, because it goes out of scope. Since our destructor closes the open file, the result is that the file is closed once open() returns. Read that last sentence again, argh.

There are two issues at play here:
1. The copy constructor doesn't work because you can't properly copy an open FILE* object.
2. Exiting the function causes the destructor to run, closing the file — despite the fact that we are returning the instance.

Can I have the attention of the class
How to solve this problem, and no ugly hacks, please. A good solution lies in using shared_ptr. You can copy a shared_ptr and safely return it, and its internal reference count will cause the File instance to stick around. The File object will not destructed until it really needs to be.
[More specifically, the shared_ptr keeps a pointer to dynamically allocated memory, which never auto-destructs like instances on the stack do. The copy constructor and operator=() of shared_ptr happily copy the pointer, but while doing so it increments a reference counter that keeps track of how many shared_ptr instances are currently referencing the dynamically allocated object. The destructor of the shared_ptr decrements the reference count, but does not delete the pointer until the refcount drops to zero. So, a shared_ptr can be used to 'keep objects around' for as long as they're needed].

Beware, you can't simply dump a FILE* into a shared_ptr, since the FILE* may be allocated with malloc() rather than with new. And besides, a FILE* opened (or allocated) with fopen() must be deallocated using fclose(). Therefore we wrap the FILE* into another new class named FileStream and use that class with the shared_ptr.
#include <cstdio>
#include <tr1/memory>

class FileStream {
public:
    FileStream(FILE *f) : f_(f) { }

    ~FileStream() {
        if (f_ != NULL)
            std::fclose(f_);
    }

    ...

private:
    FILE *f_;
};

class File {
public:

    // copy constructor copies the shared_ptr
    File(const File& f) : stream_(f.stream_) { }

    ...

private:
    std::tr1::shared_ptr<Filestream> stream_;
};

File open(const char *);
Wrapping up
This is one of the reasons why C++ is so hard. In any other language, you'd just return the basic File object and be done with it. Even simple things like returning a value is hard in C++. You need a degree in Computer Science to even attempt it.
[It makes sense when you realize that upon exiting the subroutine, the stack is cleaned and therefore the destructor is called. But we're returning a value that is on that stack ... so the value needs to be copied to another place, first].

Of course, I should've used the std::fstream class to begin with ... but std::fstream doesn't give me fdopen() nor popen(). It feels so unfinished. The documentation doesn't say whether its destructor closes the stream. I didn't feel like fixing fstream, so I went with FILE*.

C++ is a nice language but sometimes trivial things can get quite complex. Situations like this remind you that coding in C++ is really hard.