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.