Resource handles - disabling default constructors?

So, I am developing a library and have come to a dilemma. The library is private, so I can’t share it, but I think this can be a significant issue.

The dilemma was a problem due to the lack of a default constructor for the resource handling class in the library. The class processes a certain file structure, which is not very important, but allows you to call the Quake3File class.

Then, the request was to implement the default constructor and the "corresponding" Open / Close methods. My line of thinking is the RAII style, that is, if you create an instance of the class, you must give it the resource that it processes. This ensures that all successfully processed descriptors are valid and the IMO eliminates a whole class of errors.

My suggestion was to hold a (smart) pointer, and then instead of doing Open / Close and opening a can of worms, the user creates a class in free storage to “Open” it and removes it when you want to “close” it. Using a smart pointer even “closes” it to you when it goes out of scope.

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

So the questions are more or less:

  • Should resource descriptors have standard constructors?
    • What is a good way to implement the default constructor for a class that deals with files? Just set the internal state to invalid, and if the user skips it without providing him a resource, will this lead to undefined behavior? It seems strange to want to take this along the way.
  • Why does the STL implement the fstream hierarchy with default constructors? Outdated reasons?

I hope my question will be understood. Thanks.

+8
c ++ c ++ 11 default-constructor
source share
3 answers

We can say that there are two categories of RAII classes: the classes are "always valid" and "possibly empty." Most classes in standard libraries (or almost standard libraries such as Boost) fall into the latter category for several reasons, which I will explain here. By "always valid" I mean classes that must be built in a valid state and then remain valid until destroyed. And “possibly empty,” I mean classes that can be constructed in an invalid (or empty) state, or become incorrect (or empty) at some point. In both cases, the RAII principles remain, that is, the class processes the resource and implements its automatic control, since it frees the resource from destruction. Thus, from the point of view of the user, they both enjoy the same protection against resource leakage. But there are some key differences.

First of all, you need to consider that one of the key aspects of almost any resource that I can think of is that resource collection can always be unsuccessful. For example, you can not open the file, do not allocate memory, do not establish a connection, do not create a context for the resource, etc. So, you need a method to fix this potential glitch. In the "always valid" RAII class, you have no choice but to report this failure by throwing an exception from the constructor. In a "possibly empty" class, you can report this failure either by leaving the object empty or you can throw an exception. This is probably one of the main reasons the IO thread library uses this pattern because they decided to throw an exception — throwing an optional function in their classes (probably because many people are not inclined to use exceptions too often).

The second thing to consider is that “always valid” classes cannot be movable classes. Moving a resource from one object to another implies that the source object is "empty". This means that the “always valid” class must not be copyable and not movable, which can be a little annoying for users, and it can also limit your ability to provide an easy-to-use interface (for example, factory functions, etc.). It will also require the user to highlight the object on freestore whenever it needs to move the object around.

(EDIT) As indicated in DyP below, you can have an “always valid” class that can be moved if you can put an object in a destructible state. In other words, any other subsequent use of the object will be UB, only destruction will behave well. However, it remains that the class providing the “always valid” resource will be less flexible and cause some annoyance to the user. (END EDIT)

Obviously, as you noted, the “always valid” class will, in general, be more impeccable in its implementation, because you do not need to consider the case when the resource is empty. In other words, when you implement a “possibly empty” class, you must check inside each member function if the resource is valid (for example, if the file is open). But remember that "ease of implementation" is not a good reason to dictate a specific choice of interface, the interface appeals to the user. But this problem is also true for user side code. When a user is dealing with a “possibly empty” object, they should always check for correctness, and this can become problematic and error prone.

On the other hand, the “always valid” class must rely solely on exception mechanisms to report its errors (that is, the error conditions do not disappear due to the “always valid” postulate) and, therefore, there may be some interesting problems in its implementation. In general, you will need reliable security guarantees for all of your functions related to this class, including both the implementation code and the user code. For example, if you postulate that the object is “always valid” and you try to perform an operation that is not performed (for example, reading outside the file), you need to cancel this operation and return the object back to the original action to ensure that your “always valid” "postulate. In general, the user will be forced to do the same when necessary. This may or may not be compatible with the type of resource you are dealing with.

(EDIT) As indicated in DyP below, there are shades of gray between these two types of RAII classes. So, note that this explanation describes two pole opposites or two general classifications. I am not saying that this is a black and white difference. Obviously, many resources have varying degrees of "certainty" (for example, an invalid file handler can be in the "not open" state or in the "reached end of file" state, which can be processed differently, that is, "always open", "possibly in EOF", file handler class). (END EDIT)

Should resource descriptors really have default constructors?

The default constructors for RAII classes are usually understood as creating an object in the "empty" state, which means that they are valid only for "possibly empty" implementations.

What is a good way to implement the default constructor for a class that deals with files? Just set the internal state to invalid, and if the user skips it without providing him a resource, will this lead to undefined behavior? It seems strange to want to take it off this route.

Most of the resources I've ever come across have a natural way of expressing “emptiness” or “invalidity”, be it a null pointer, a null file descriptor, or just a flag to mark the state as valid or not. So this is easy. However, this does not mean that misuse of the class should cause "undefined behavior". It would be terrible to create such a class. As I said earlier, there are errors that can occur, and making the class “always valid” does not change this fact, but only the means with which you deal with them. In both cases, you should check the error conditions and report them, as well as fully determine the behavior of your class in case of their occurrence. 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? Outdated reasons?

At first, the IO stream library is not part of the STL (Standard Template Library), but this is a common mistake. In any case, if you read my explanations above, you will probably understand why the IO stream library chose what it does. I think that this essentially boils down to eliminating exceptions as a necessary, fundamental mechanism for their implementation. They allow exceptions as an option, but they do not make them mandatory, and I think that this should have been a requirement for many people, especially in the days when it was written, and probably still.

+5
source share

I think that each case should be considered separately, but for the file class I would probably think of introducing an “invalid state”, for example, “the file cannot be opened” (or “the file is not attached to the wrapper handler class”)).

For example, if you do not have this “invalid file” state, you force the file upload method or function to exclude exceptions when the file cannot be opened. I don't like this because the caller has to use a lot of try/catch wrappers around the file upload code, and 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, eg 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, eg use some default settings for f1... doSomething(); File f2 = loadFile("foo2"); if (! f2.valid()) ... handle load failure 

In addition, it may also make sense to make the File class movable (so you can also put File instances in containers, for example, have std::vector<File> ), in which case you should have an “invalid” state for the transferred instance file.

So, for the File class, I would find that introducing an invalid state is ok.

I also wrote a raw RAII pattern wrapper, and I also implemented an invalid state. Again, this also allows you to correctly implement the semantics of movement.

+4
source share

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

Almost the only argument in favor of having a standard ctor for such a class is that it simplifies their assembly. With the semantics of movement and the ability to place objects, this becomes much less convincing. It was never really necessary, and now it almost does not matter.

+1
source share

All Articles