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.