An object can only exist for a subset of the time for which its memory is allocated. An object's lifetime is defined as the closed interval from the end of its construction to the start of its destruction. Using a member of an object outside its lifetime is undefined behaviour.
For a local ordinary object, lifetime is limited by the scope it is defined in and can be easily seen with messages from constructor and destructor.
#include <iostream>
class Tracer
{
public:
Tracer()
{
std::cout << "lifetime begins\n";
}
// Run this destructor when the object lifetime ends
~Tracer()
{
std::cout << "lifetime ends\n";
}
};
int main()
{
std::cout << "before the object\n";
Tracer t{};
std::cout << "inside the lifetime\n";
return 0;
}
The "before the object" message is executed first. Tracer t{}; first allocates the space, then calls the constructor which executes and the message "lifetime begins" is written only after. Then, since the object lifetime has been initiated, the line std::cout << "inside the lifetime\n"; is allowed to execute.
When the scope of main is exited, the destructor is called which executes "lifetime ends" before the space allocated is deallocated.
The object can be safely used only in the scope between these two messages.
The same applies to objects defined inside other scopes. When one block is entered while the outer scope is still executing, an inner lifetime will be strictly nested inside the outer one.
#include <iostream>
class Tracer
{
private:
int id{};
public:
Tracer(int i)
{
id = i;
std::cout << "begin " << id << '\n';
}
// Run this destructor when the object lifetime ends
~Tracer()
{
std::cout << "end " << id << '\n';
}
};
int main()
{
Tracer outer{ 1 };
{
Tracer inner{ 2 };
std::cout << "inside inner block\n";
}
std::cout << "back in outer block\n";
return 0;
}
The first constructor called will be outer, which will print "begin 1". After this message, the program enters the nested block, creating inner which prints "begin 2". Then, while both objects still exist, the message "inside inner block" is written.
When the nested block is exited, inner's destructor is called and "end 2" is written.
The execution continues in the outer block which prints "back in outer block" before main finally ends and calls the destructor of outer, printing "end 1". The lifetime of inner is contained inside that of outer.
When two or more objects are defined in the same scope, they are first constructed in the order of their definition and destroyed in the reverse order, meaning their lifetimes will overlap but they will each end at a different time.
#include <iostream>
// Define the members that belong to this type
class Tracer
{
private:
int id{};
public:
Tracer(int i)
{
id = i;
std::cout << "begin " << id << '\n';
}
~Tracer()
{
std::cout << "end " << id << '\n';
}
};
int main()
{
Tracer a{ 1 };
Tracer b{ 2 };
Tracer c{ 3 };
std::cout << "all alive\n";
return 0;
}
First, the three objects are constructed and their constructors print "begin 1", "begin 2" and "begin 3". After the message "all alive", the block ends, the destructors of c, b and a are called in this order and "end 3", "end 2" and "end 1" are printed. Object c has the shortest lifetime and object a the longest.
If the program logic is to call code between the end of construction and the start of destruction for at least one of the three objects, then it may try to use any of the three.
However, if it tries to use object c after its destructor has been called, while a and b are still alive, this becomes undefined behaviour.
The compiler will automatically call the constructor at the point of declaration and call the destructor at the point at which the objects leave scope. It's the programmer's responsibility neither to call them. If the constructor for an object throws an exception, its lifetime never begins and destructor will never be called. If an object's destructor throws, the program is exiting the block which means control flow has already left the block which makes further consequences something that is not really within the limits of “normal” control flow, for the sake of these problems.
An object name is no longer valid as soon as its lifetime ends.
This is because the name is bound to an object of that class's lifetime. The memory used for the object can continue to be reserved by the stack frame for a short while; however, for language purposes, it is assumed the object no longer exists.
Exercise: Object LifetimeStarter code, hints and solutionOpenClose
Practice: Object Lifetime
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Create a Tracer with id 4 in an inner block. Print a message inside and after that block to show its lifetime ending before the original objects.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
// Define the members that belong to this type
class Tracer
{
private:
int id{};
public:
Tracer(int i)
{
id = i;
std::cout << "begin " << id << '\n';
}
~Tracer()
{
std::cout << "end " << id << '\n';
}
};
int main()
{
Tracer a{ 1 };
Tracer b{ 2 };
Tracer c{ 3 };
std::cout << "all alive\n";
return 0;
}Hints
a, b, and c are all alive at the "all alive" output. Their automatic lifetimes end when control leaves main.
To see an earlier lifetime end, place a new Tracer inside its own nested braces. Its end message appears at that inner closing brace, before the outer objects are destroyed.
Worked solution
A fourth Tracer is created inside a nested block. Its end 4 message occurs at the inner closing brace, before Outside inner block; the original three remain alive until main ends.
#include <iostream>
// Define the members that belong to this type
class Tracer
{
private:
int id{};
public:
Tracer(int i)
{
id = i;
std::cout << "begin " << id << '\n';
}
~Tracer()
{
std::cout << "end " << id << '\n';
}
};
int main()
{
Tracer a{ 1 };
Tracer b{ 2 };
Tracer c{ 3 };
std::cout << "all alive\n";
{
Tracer inner{4};
std::cout << "Inside inner block\n";
}
std::cout << "Outside inner block\n";
return 0;
}
Example output
begin 1
begin 2
begin 3
all alive
begin 4
Inside inner block
end 4
Outside inner block
end 3
end 2
end 1
0 comments
No comments yet. Be the first.