Discussion: A False Start
Execute the code to understand the output and gain insights into object and resource management.
Run the code
Now, it’s time to execute the code and observe the output.
#include <iostream>#include <stdexcept>struct Engine{~Engine() { std::cout << "Engine stopped\n"; }};struct Machine{Machine() { throw std::runtime_error{"Failed to start the machine"}; }~Machine() { std::cout << "Machine stopped\n"; }Engine engine_;};int main(){try{Machine machine;}catch (...){}}
Understanding the code
In this puzzle, we have a Machine
class that contains an Engine
. But Machine
throws in its constructor before it’s completely done initializing. Does the engine get stopped? Does the Machine
itself get stopped?
Object destruction
One of the great things about C++ is that constructed objects get destroyed automatically and deterministically (unless we manually call new, which these days should be left to library authors). We’ll find no need for Python’s with
or C#’s using
, and rarely a need for something like finally
.
But only constructed objects get destroyed, so what does it mean to be constructed? And what happens in the case of half-constructed objects like the Machine
instance here, which throws during construction?
Order of object initialization
An object is only deemed to be constructed when initialization has been completed. The initialization of a class object goes in this order:
Virtual base classes
Direct base classes
Non-static data members (like
Machine::engine_
)The constructor body
Initialization of Machine
So, when we’re constructing the Machine
object, we first initialize the Engine
member. As soon as this is done, engine_
is deemed constructed and will eventually have to be destroyed. Then, we get to the Machine
constructor body, which throws an exception. The initialization of Machine
did not complete, and it’s not deemed constructed. Only the Engine
destructor gets called, not the Machine
destructor.
This is very useful since we don’t have to think about cleaning up half-initialized objects. Any initialized member like engine_
gets destroyed automatically; the rest don’t need to be since they were never constructed in the first place. We do have to be careful, though, if we manually acquire some resources in the constructor. In that case, we can’t rely on the destructor to clean it up. For instance, if we call new
and assign the pointer to a member, we cannot rely on a delete
in our destructor to clean it up. The same goes for raw calls to function pairs like open
/close
.
Automatic resource management
As always, to make our code more robust, avoid manually calling functions like delete
and close
. Instead, use standard library types like unique_ptr
or fstream
as members, which automatically clean up the managed resources in their destructors. If we have several such resource members, the ones that got initialized—and only those—will be automatically released even in the case of a half-constructed object like machine
.
If our class manages a custom resource that doesn’t fit in a standard library type, such as the engine in this puzzle that needs to be stopped, factor that responsibility out to a separate small class like Engine
instead of handling it manually in the containing class. Then, we don’t need to think about managing it at all; it will always be destroyed if it is constructed.
Recommendations
Here are some recommendations to ensure proper resource management and avoid memory leaks.
Avoid manual resource management: Use standard library types like
unique_ptr
andfstream
as members to automatically manage resources and prevent leaks.Encapsulate custom resource management: For custom resources, create dedicated classes focused solely on managing that specific type of resource, ensuring proper handling and cleanup.
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.