Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resource handles - prohibiting default constructors?

So, I've been doing some library development and came to a dilemma. Library is private, so I can't share it, but I feel this could be a meaningful question.

The dilemma presented itself as an issue over why there is no default constructor for a resource handling class in the library. The class handles a specific file structure, which is not really important, but lets call the class Quake3File.

The request was then to implement a default constructor and the "appropriate" Open/Close methods. My line of thinking is RAII style, that is if you create a instantiation of the class you must give it a resource that it handles. Doing this ensures that any and all successfully constructed handles are valid and IMO eliminates a whole class of bugs.

My suggestion was to keep a (smart)pointer around and then instead of having to implement a Open/Close and open a can of worms, the user creates the class on the free store to "Open" it and deletes is when you want to "Close" it. Using a smart pointer will even "Close" it for you when it gets out of scope.

This is where the conflict comes in, I like to mimic the STL design of classes, since that makes my classes easier to use. Since I'm making a class that essentially deals with files and if I take std::fstream as a guide, then I am not sure if I should implement a default constructor. The fact that the entire std::fstream hierarchy does it points me to a Yes, but my own thinking goes to No.

So the questions are more or less:

  1. Should resource handles really have default constructors?
    • What is a good way implement a default constructor on a class that deals with files? Just set the internal state to an invalid one and if a user missuses it by not giving it a resource have it result in undefined behavior? Seems strange to want to take it down this route.
  2. Why does the STL implement the fstream hierarchy with default constructors? Legacy reasons?

Hope my question is understood. Thanks.

like image 803
Honf Avatar asked Dec 04 '13 19:12

Honf


3 Answers

One could say that there are two categories of RAII classes: "always valid" and "maybe empty" classes. Most classes in standard libraries (or near-standard libraries like Boost) are of the latter category, for a couple of reasons that I'll explain here. By "always valid", I mean classes that must be constructed into a valid state, and then remain valid until destruction. And by "maybe empty", I mean classes that could be constructed in an invalid (or empty) state, or become invalid (or empty) at some point. In both cases, the RAII principles remain, i.e., the class handles the resource and implements an automatic management of it, as in, it releases the resource upon destruction. So, from a user's perspective, they both enjoy the same protections against leaking resources. But there are some key differences.

First thing to consider is that one key aspect of almost any resource that I can think of is that resource acquisition can always fail. For example, you could fail to open a file, fail to allocate memory, fail to establish a connection, fail to create a context for the resource, etc.. So, you need a method to handle this potential failure. In an "always valid" RAII class, you have no choice but to report that failure by throwing an exception from the constructor. In a "maybe empty" class, you can choose to report that failure either through leaving the object in an empty state, or you can throw an exception. This is probably one of the main reasons why the IO-streams library uses that pattern, because they decided to make exception-throwing an optional feature in its classes (probably because of many people's reticence to using exceptions too much).

Second thing to consider is that "always valid" classes cannot be movable classes. Moving a resource from one object to another implies making the source object "empty". This means that an "always valid" class will have to be non-copyable and non-movable, which could be a bit of an annoyance for the users, and could also limit your own ability to provide an easy-to-use interface (e.g., factory functions, etc.). This will also require the user to allocate the object on freestore whenever he needs to move the object around.

(EDIT) As pointed out by DyP below, you could have an "always valid" class that is movable, as long as you can put the object in a destructible state. In other words, any other subsequent use of the object would be UB, only destruction will be well-behaved. It remains, however, that a class that enforces an "always valid" resource will be less flexible and cause some annoyance for the user. (END EDIT)

Obviously, as you pointed out, an "always valid" class will be, in general, more fool-proof in its implementation because you don't need to consider the case where the resource is empty. In other words, when you implement a "maybe empty" class you have to check, within each member function, if the resource is valid (e.g., if the file is open). But remember that "ease of implementation" is not a valid reason to dictate a particular choice of interface, the interface faces the user. But, this problem is also true for code on the user's side. When the user deals with a "maybe empty" object, he always has to check validity, and that can become troublesome and error-prone.

On the other hand, an "always valid" class will have to rely solely on exception mechanisms to report its errors (i.e., error conditions don't disappear because of the postulate of "always valid"), and thus can present some interesting challenges in its implementation. In general, you will have to have strong exception safety guarantees for all your functions involving that class, including both implementation code and user-side code. For example, if you postulate that the object is "always valid", and you attempt an operation that fails (like reading beyond the end of a file), then you need to roll back that operation and bring the object back to its original valid state, to enforce your "always valid" postulate. The user will, in general, be forced to do the same when relevant. This may or may not be compatible with the kind of resource you are dealing with.

(EDIT) As pointed out by DyP below, there are shades of grey between those two types of RAII classes. So, please note that this explanation is describing two pole-opposites or two general classifications. I am not saying that this is a black-and-white distinction. Obviously, many resources have varying degrees of "validity" (e.g., an invalid file-handler could be in a "not opened" state or a "reached end-of-file" state, which could be handled differently, i.e., like a "always opened", "maybe at EOF", file-handler class). (END EDIT)

Should resource handles really have default constructors?

Default constructors for RAII classes are generally understood as creating the object into an "empty" state, meaning that they are only valid for "maybe empty" implementations.

What is a good way implement a default constructor on a class that deals with files? Just set the internal state to an invalid one and if a user missuses it by not giving it a resource have it result in undefined behavior? Seems strange to want to take it down this route.

Most resources that I have ever encountered have a natural way to express "emptiness" or "invalidity", whether it be a null pointer, null file-handle, or just a flag to mark the state as being valid or not. So, that's easy. However, this does not mean that a misuse of the class should trigger "undefined behavior". It would be absolutely terrible to design a class like that. As I said earlier, there are error conditions that can occur, and making the class "always valid" does not change that fact, only the means by which you deal with them. In both cases, you must check for error conditions and report them, and fully specify the behavior of your class in case they happen. You cannot just say "if something goes wrong, the code has 'undefined behavior'", you must specify the behavior of your class (one way or another) in case of error conditions, period.

Why does the STL implement the fstream hierarchy with default constructors? Legacy reasons?

First, the IO-stream library is not part of the STL (Standard Template Library), but that's a common mistake. Anyway, if you read my explanations above you will probably understand why the IO-stream library chose to do things the way it does. I think that it essentially boils down to avoiding exceptions as a required, fundamental mechanism for their implementations. They allow exceptions as an option, but don't make them mandatory, and I think that must have been a requirement for many people, especially back in the days it was written, and probably still today.

like image 63
Mikael Persson Avatar answered Nov 02 '22 13:11

Mikael Persson


I think that each case should be considered separately, but for a file class I'd certainly consider the introduction of an "invalid state" like "file cannot be opened" (or "no file attached to the wrapper handler class").

For example, if you don't have this "invalid file" state, you will force a file loading method or function to throw exceptions for the case that a file cannot be opened. I don't like that, because the caller then should use lots of try/catch wrappers around file loading code, while instead a good-old boolean check would be just fine.

// *** I don't like this: ***

try
{
    File f1 = loadFile("foo1");
}
catch(FileException& e)
{
    ...handle load failure, e.g. use some defaults for f1
}

doSomething();

try
{
    File f2 = loadFile("foo2");
}
catch(FileException& e)
{
    ...handle load failure for f2
}

I prefer this style:

File f1 = loadFile("foo");
if (! f1.valid())
  ... handle load failure, e.g. use some default settings for f1...

doSomething();

File f2 = loadFile("foo2");
if (! f2.valid())
  ... handle load failure

Moreover, it may also make sense to make the File class movable (so you may also put File instances in containers, e.g. have a std::vector<File>), and in this case you must have an "invalid" state for a moved-from file instance.

So, for a File class, I'd consider the introduction of an invalid state to be just fine.

I also wrote a RAII template wrapper to raw resources, and I implemented an invalid state there as well. Again, this makes it possible to properly implement move semantics, too.

like image 29
Mr.C64 Avatar answered Nov 02 '22 11:11

Mr.C64


At least IMO, your thinking on this subject is probably better than that shown in iostreams. Personally, if I were creating an analog of iostreams from scratch today, it probably would not have a default ctor and separate open. When I use an fstream, I nearly always pass the file name to the ctor rather than default-constructing followed by using open.

Nearly the only point in favor of having a default ctor for a class like this is that it makes putting them into a collection easier. With move semantics and the ability to emplace objects, that becomes much less compelling though. It was never truly necessary, and is now almost irrelevant.

like image 1
Jerry Coffin Avatar answered Nov 02 '22 11:11

Jerry Coffin