Constructors and destructors are mechanisms of the RAII technique, they are not the purpose of this idiom.
To better understand RAII you need to understand where the need for RAII came from and what problem does RAII actually solve.
The original problem
For this consider C. You have a library that thought it's API it gives you a resource:
const char * some_lib_get_last_error_message();
The resource you are given is the string denoted by the pointer returned by the function. Now answer this question: is the object a resource needing manual lifetime management? If so who is responsible for its creation/destruction? A raw pointer is not powerful enough to express this properties so the question is impossible to answer (without looking at the implementation or documentation).
The string could be an object with static storage duration. In this case the user of the library must not "clean" this resource in any way. Doing so (e.g. free) would cause serious problems. Or the object could be an object with dynamic storage duration. If that is the case then we have another dilemma: who is responsible to clean it and how is the cleaning supposed to be done. It could be that the library cleans it somehow. If that is the case the user cleaning it would result in serious problems. It could be that the user must clean it. In this case if the user forgets to clean it then that would result in serious problems. Then there is the problem of how the user is supposed to clean it. It could require a free or it could require a call to some other library API. Moreover there could be other conditions that govern when the cleaning of this resource can be done. The user could be required to do some other actions before cleaning it or it could be required to not clean it before some other events.
This is a problem in C and the way it's handled is through documentation. The library must document every resource the user must release, how to do so and when it is allowed/required to do so.
The problem is aggravated in C++
With C++ the problem becomes even more serious: because of exceptions C++ has many many hidden exit points from a function. So ensuring a resource is released as needed becomes near impossible with the C idiom. Consider:
auto user_function()
{
auto resource_r1 = acquire_resource_r1();
A a = foo(X{}, Y{}, resource_r1);
auto resource_r2 = acquire_resource_r2();
B b = bar("text", resource_r2);
C c = a + b;
c.use(resource_r1, resournce_r2);
release_r1(resource_r1);
release_r2(resource_r2)
}
While this would have been fine in C, in C++ this is incorrect code. Depending on the types involved: acquire_resournce_r1 could throw, X constructor could throw, Y constructor could throw, the conversion from X to the 1st param type of foo could throw, the conversion from Y to the 2nd param type of foo could throw, the conversion from resource_r1 to the 3rd param type of A could throw, foo could throw, acquire_resource_2 could throw, the conversion from the type returned from foo to A could throw, the constructor of the 1st param type of bar could throw, the conversion from resource_r2 to the 2nd param type of bar could throw, bar could throw, the conversion from the type returned by bar to b could throw, the operator + could throw, the conversion from the type returned by + to C could throw, the conversion from resource_r1 to the 1st param type of use could throw, the conversion from resource_r2 to the 1st param type of use could throw, use could throw, release_r1 could throw, release_r2 could throw. Yes, there can be about 20 hidden exit points in the above function.
So how do you ensure that resource_r1 and resource_r2 are always correctly cleaned? Using the C idiom to do so would be nothing short of a nightmare. Think what kind of try/catch monstrosity would be required here to correctly clean resource_r1 an resource_r2. It would also completely defeat the purpose of exceptions as you would need to catch exceptions just to correctly do the cleanup, even if you can't nor want to treat the errors in any way.
Not to mention that it would still have the same problem as in C: you don't know who's responsible for cleaning the resources.
The C++ solution
Bjarne Stroustrup and Andrew Koenig came with an ingenious and elegant solution: bind the lifetime of the resources to the lifetime of objects. And use objects with automatic, thread or static storage duration. An object has two lifetime events: construction and destruction; this are done automagically by the compiler in the case of objects with automatic, thread and static storage duration. A resource has two lifetime events: acquisition and release; these need to be done manually. So RAII binds the acquisition of a resource to the construction of an object and the release of a resource to the destruction of the resource. Now the compiler will do everything for you: it will properly clean the resource.. correctly.. regardless of exceptions thrown.. or order of acquisition. Not only this is done correctly, but the user of the resource must not bother at all with the manual management of the resource lifetime.
This brings us to the concept of ownership. Remember how in C there was no clear entity responsible with the destruction of the resource. With RAII there is always (at least) an owner of every resource: the object to which the lifetime of the resource is bound to. This not only solves the problem of "who must clean" but also "how to clean". The owner of the resource is responsible with the cleaning and knows how to clean the resource.
The most important concept in RAII is ownership. As long as a resource has an owner the resource acquisition/release is always done correctly.
Conclusion
So, so sum it up:
- A resource must always be owned by an object (or by multiple objects in a shared ownership).
- The most common point to acquire the resource is in the object construction. However that is not always the case. An object can be created without a resource and the resource can be acquired during the lifetime of the object.
- The most common point to release the resource is in the destructor of the object that owns it. However that is not always the case. The resource can be manually released by the user before the destruction of the object. Regardless, the object type that is a resource owner must always check in the destructor if it still owns a resource and clean it if it does.
- the owner of a resource can change during the lifetime of a resource. Objects can swap or steal resources from one another. As long as no resource ends up orphan (without an owner) everting is ok.
- the C++ mechanisms of destructors, object lifetime, scope exit, order of initialization and stack unwinding are leveraged by RAII to ensure always proper and correct resource cleanup, without any action from the user.