Last time we discussed Value Semantics. However, I missed one topic that is super important for a better understanding of basic building blocks of C++. Today, we are going to talk about an object. Without further ado, let’s dive deeper!
Object
What is an object? According to the C++ standard, part 3.9.8 under the name of [basic.types]
Now is int i
an object? Yes.
Is void* p
an object? Still yes, because pointers are types themselves, they are not references.
As we said, references are not types, but what if we declare something like struct S{ int& ref;};
would that be an object type? Put your answers down below, I'd like to see your opinion on this. :)
Lifetime
When we talk about objects, the one idea that always follows is lifetime.
Lifetime is a period in which an object is fully initialized. It begins with
- storage with the proper alignment,
- obtaining size for type T, and
- if the object has non-trivial initialization, its initialization is complete.
The lifetime of an object of type T ends when:
-
T is a class type with a non-trivial destructor,
-
the destructor call starts, or
- the storage which the object occupies is reused or released
This is an extraction from [basic.life] paragraph of C++ standard. In easy words, complex objects live from the end of the constructor, until the beginning of a destructor, and the trivial objects, like int
live from their storage allocation, until the end of the scope. Seems pretty easy, right?
So, if you see something like:
you'd probably say that there is something fishy about it, and you'd be correct. Although i
is returned from a function, notice the auto&
here. It says that we're actually returning a reference to a local variable. After the function returns, the stack is cleaned. Hence, you invoke a good old UB.
But what if we change the function to return a string instead of an int?
Now we get a normal "Hello" string from a function. This is due to a simple rule of a language, that states that all strings are valid throughout the entire program because they have a static storage.
But let's change the story again:
Notice that I've changed only one character and suddenly everything breaks. The behavior is undefined, everything crashes and burns. What happened here? We actually initialized the array with a copy of static data and returned a pointer to it.
Complex objects and their lifetimes
As we already discussed, lifetime of a complex object begins with a constructor and ends with a destructor. Although there are objects that have implicit destructors and constructors, there still may be things that may surprise you.
Firstly, the lifetime begins with the ending of the constructor execution.
That means a lot. Let's have a look at an example:
I think everyone will agree that the call order is constructor, and then destructor. But what if I present exceptions to the equation? What if S()
may throw?
Well, the answer is: the destructor does not start! Why? Because the lifetime of an object did not start, since the constructor didn't finish its execution.
Here lies a menace:
Are widgets deleted with the unwinding if the second object throws? No, the memory will leak. Hence, this is why the usage of new
operator is a bad code smell and we should avoid it whenever possible. Here is an example on Godbolt.
However, a good thing to mention is that the stack unwinder actually destroys all initialized objects from within the class on throw. So, the pointer being an object type is trivially destroyed, but not the memory behind it.
What can we do to make this code work? Delegating constructors comes into play!
Here, the delegation constructor finished its execution, hence the object is alive, and the destructor finishes successfully on throw. This information brings us to the point of smart pointers. Since in many cases you can't have an additional constructor to delegate to, or even a constructor at all, you need smart pointers to have robust code and ensure correct ownership of the objects.
Conclusion
Today we have touched a very basic yet complex topic of object lifetimes. Of course, there is a lot more to say and we will continue basics in the next post tackling move semantics with lifetime implications and finishing with a sprinkle of ownership semantics of smart pointers.
I hope you enjoyed the read. There is a plan for a video series that tackles modern C++ 20/23 topics and use the concepts in practice. Stay tuned for the announcement. :)
Be careful when returning references and wrapper objects from a function, and use new
as rarely as possible! Enjoy object lifetime!
Sincerely,
Ilya "Agrael" Doroshenko
Trusted software excellence across embedded and desktop platforms
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.