4. Object Lifetime and Resource ManagementIntroduction and ContextObject-oriented programming is often introduced through classes, inheritance, polymorphism, and encapsulation. Yet these mechanisms become reliable only when a program also manages the lifetime of its objects correctly. An object is not merely a collection of data and functions. It occupies storage, may acquire external resources, and must eventually release those resources in a predictable manner. Errors in this area produce memory leaks, dangling pointers, double deletion, use-after-free defects, resource exhaustion, and difficult-to-reproduce failures. C++ gives programmers unusually precise control over object lifetime. This control is powerful because it supports systems programming, high-performance computing, embedded software, operating systems, and real-time applications. It is also demanding: the programmer must understand when storage is obtained, when an object begins and ends its lifetime, which constructor or destructor is invoked, and who is responsible for releasing an acquired resource. The central design principle of this chapter is Resource Acquisition Is Initialization, commonly abbreviated RAII. Under RAII, ownership of a resource is attached to an object, acquisition occurs during construction, and release occurs during destruction. Combined with automatic storage duration, smart pointers, move semantics, and carefully designed copy operations, RAII allows deterministic resource management without scattering cleanup code throughout a program. Learning ObjectivesAfter studying this chapter, the reader should be able to: 1. Distinguish storage duration, scope, lifetime, and ownership. 2. Explain construction, destruction, object lifetime boundaries, and the order in which subobjects are handled. 3. Apply RAII to memory, files, locks, sockets, transactions, and other resources. 4. Select appropriate ownership models using raw pointers, references, `std::unique_ptr`, `std::shared_ptr`, and `std::weak_ptr`. 5. Analyse copy and move semantics and explain the Rule of Zero, Rule of Three, and Rule of Five. 6. Identify common lifetime errors, including dangling references, double deletion, cycles, and exception-related leaks. 7. Design resource-managing classes that are exception-safe, maintain clear ownership, and fit professional software-development practices. Storage, Lifetime, and Ownership: Related but Different IdeasThree concepts are frequently confused. Storage duration describes how long the storage associated with an object is available. Object lifetime describes the interval during which an object has been constructed and can legally be used. Ownership describes which entity is responsible for managing a resource. An object with automatic storage duration is normally created when execution reaches its declaration and destroyed when the enclosing scope is left. An object with dynamic storage duration is created through allocation, such as `new`, and remains alive until explicitly destroyed, normally through `delete` or through an owning abstraction. Static storage duration applies to objects that exist for the duration of the program. Thread storage duration applies to objects that exist independently for each thread. These categories should not be treated as interchangeable. A pointer variable may have automatic storage duration while pointing to an object with dynamic storage duration. For example: ```cpp Widget* p = new Widget(); ``` The pointer `p` is an automatic local variable, but the `Widget` object is dynamically allocated. When the scope ends, `p` disappears; the `Widget` does not. If no other pointer refers to it, it becomes a leak. This distinction is one reason direct use of `new` and `delete` should generally be replaced by ownership-aware abstractions. A reference does not own the object to which it refers. Similarly, a raw pointer does not by itself reveal whether it is an owner, a non-owning observer, an optional value, or a pointer into another object. This ambiguity is a major source of maintenance problems. Modern C++ uses types and interfaces to express ownership explicitly wherever possible. Construction and Destruction as Lifetime BoundariesAn object’s lifetime begins after its initialization has completed sufficiently for the object to exist as an instance of its type. Its lifetime ends when its destructor begins, or when the object’s storage is reused or released in a manner that ends its lifetime. Accessing an object outside this interval is undefined behavior, even if the bytes formerly occupied by the object still appear to contain familiar values. A constructor establishes class invariants. It should either create a valid object or fail, commonly by throwing an exception. A destructor releases resources and should normally not throw. If a destructor allows an exception to escape while another exception is already propagating, the program may terminate. Consequently, destructors should perform reliable cleanup operations and handle failures internally when failure is possible. Initialization occurs before the constructor body. Data members are initialized in the order in which they are declared in the class, not in the order written in the constructor’s member-initializer list. Base classes are initialized before derived-class members, and members are initialized before the constructor body executes. Destruction occurs in the reverse order: the destructor body runs first, then members are destroyed in reverse declaration order, and finally base-class subobjects are destroyed in reverse inheritance order. The following example illustrates the principle: ```cpp class Session { Logger logger; Socket socket; public: Session() : socket(), logger() {} }; ``` Although `socket` appears first in the initializer list, `logger` is constructed first because it is declared first. At destruction, `socket` is destroyed before `logger`. Correct declaration order is therefore important when one member depends on another. For polymorphic base classes, a base class intended for deletion through a base pointer should generally have a virtual destructor: ```cpp class Shape { public: virtual ~Shape() = default; virtual double area() const = 0; }; ``` Without a virtual destructor, deleting a derived object through a base pointer can fail to destroy the derived part correctly, producing resource leaks or undefined behavior. RAII: The Governing Design PatternRAII binds a resource to an object whose destructor releases it. The resource may be memory, a file descriptor, a mutex lock, a database connection, a socket, a temporary directory, or a transaction. The object’s lifetime then becomes the boundary of the resource’s ownership. ```cpp #include <fstream> #include <string> void append_record(const std::string& name, const std::string& text) { std::ofstream file(name, std::ios::app); if (!file) { throw std::runtime_error("file could not be opened"); } file << text << '\n'; } ``` If writing throws or the function returns normally, the stream’s destructor closes the file. There is no separate cleanup path for every possible return statement or exception. This is the main advantage of RAII: cleanup is attached to scope exit rather than to the programmer’s ability to remember every control-flow path. RAII also supports exception safety. Suppose a function creates several resources and then an operation fails. Local objects already constructed are destroyed automatically during stack unwinding. A class whose members are RAII objects can therefore remain safe without a large, manually written rollback routine. A well-designed RAII type usually has the following properties: - Construction acquires or establishes the resource.
- Destruction releases the resource exactly once.
- The object maintains a clear invariant while alive.
- Copying is either meaningful and safe or disabled.
- Moving transfers responsibility without duplicating the resource.
- Destruction is deterministic and normally non-throwing.
RAII is broader than memory management. `std::lock_guard` applies it to mutex ownership, `std::fstream` to file ownership, and `std::unique_ptr` to dynamically allocated objects. The same reasoning can be applied to operating-system handles and third-party library resources by writing suitable wrapper classes. Ownership Models and Smart PointersOwnership is a design decision, not merely a pointer representation. Before selecting a pointer type, determine whether an object has one owner, several owners, no owner in the current context, or an optional relationship. Unique ownership`std::unique_ptr<T>` represents exclusive ownership. It cannot be copied, but it can be moved. When the `unique_ptr` is destroyed, it deletes its managed object using its deleter. ```cpp #include <memory> std::unique_ptr<int> make_value() { return std::make_unique<int>(42); } ``` Returning a `unique_ptr` is efficient because the ownership is transferred through move semantics. A class containing a `unique_ptr` normally should not need a handwritten destructor, copy constructor, or move constructor. The standard library member manages the resource automatically. A custom deleter allows `unique_ptr` to manage resources not released by ordinary `delete`: ```cpp struct FileCloser { void operator()(std::FILE* file) const noexcept { if (file) std::fclose(file); } }; using FileHandle = std::unique_ptr<std::FILE, FileCloser>; ``` Shared ownership`std::shared_ptr<T>` represents shared ownership. A control block records the managed object and a reference count. The object is destroyed when the last owning `shared_ptr` is destroyed or reset. Shared ownership is appropriate when independent components genuinely need to extend the same object’s lifetime. It should not be used merely because ownership has not been analysed. Reference counting introduces control-block overhead and can obscure the architecture. It also cannot reclaim a cycle of strong references. Prefer `std::make_shared<T>(...)` when possible. It usually performs allocation efficiently and constructs the object directly under shared ownership. However, the lifetime of the object and the control block can have different practical implications, especially when weak references or custom allocation strategies are involved. Non-owning observationA raw pointer or reference is suitable when the referred object is owned elsewhere and the interface guarantees that it remains alive during use. Such an observer must not delete the object. A pointer may also represent absence, while a reference normally represents a required relationship. `std::weak_ptr<T>` is a non-owning observer of an object managed by `shared_ptr`. It is especially useful for breaking ownership cycles and for caches or registries that should not keep an object alive. Before use, it must be converted through `lock()`: ```cpp if (auto owner = weak.lock()) { owner->process(); } ``` The resulting `shared_ptr` temporarily establishes ownership while the object is being used. A practical ownership decisionA useful decision sequence is: 1. Can the object be stored directly as a member or local variable? Prefer that arrangement. 2. If dynamic allocation is required, is there exactly one owner? Use `unique_ptr`. 3. Is shared lifetime an explicit domain requirement? Use `shared_ptr`. 4. Is the relationship non-owning? Use a reference, raw pointer, or `weak_ptr` according to nullability and shared-control-block requirements. 5. Can the interface express ownership through a value, reference, span, or view instead of a pointer? Prefer the clearer abstraction. Copy Semantics, Move Semantics, and Resource HandlesCopying creates a second value with an equivalent state. For resource-owning objects, this raises a central question: should the resource itself be duplicated, or should ownership be transferred? A shallow copy of a raw resource handle is usually unsafe because two objects may attempt to release one resource. Consider a class that stores a dynamically allocated array through a raw pointer. A compiler-generated copy constructor copies the pointer value, not the array. Both objects then point to the same allocation. Destruction of the first object leaves the second with a dangling pointer, and destruction of both may cause double deletion. This is why resource-owning classes require deliberate copy policy. A deep copy creates an independent resource. A move transfers the resource from a source object to a destination object, leaving the source valid but generally empty. Move operations are particularly valuable for containers and return values because they avoid unnecessary allocation and copying. ```cpp class Buffer { std::size_t size_ = 0; std::unique_ptr<std::byte[]> data_; public: explicit Buffer(std::size_t n) : size_(n), data_(n ? std::make_unique<std::byte[]>(n) : nullptr) {} std::size_t size() const noexcept { return size_; } std::byte* data() noexcept { return data_.get(); } }; ``` This class follows the Rule of Zero. Because `unique_ptr` owns the allocation, `Buffer` requires no explicit destructor. Its copy operations are implicitly deleted because `unique_ptr` is non-copyable, while its move operations are available if the class’s other members permit them. If copying is required, it can be implemented explicitly by allocating a new buffer and copying the elements. The standard operations relevant to resource-owning types are: - Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
The Rule of Three states that if a class must explicitly define one of destructor, copy constructor, or copy assignment operator, it will often need all three. The Rule of Five extends this reasoning by including move constructor and move assignment operator. The modern Rule of Zero recommends designing a class from members that already manage resources, so that the class defines none of these special functions. The Rule of Zero is generally the preferred design. The Rule of Five remains necessary for low-level resource wrappers, custom memory management, polymorphic ownership mechanisms, or performance-sensitive types whose representation cannot be delegated to standard RAII components. Move operations should preserve invariants. A moved-from object remains destructible and assignable, but its exact value is generally unspecified unless the type documents stronger guarantees. A moved-from `std::unique_ptr` is empty; a moved-from standard string remains valid but should not be assumed to contain a particular value. Exception Safety and Transactional ThinkingResource management must remain correct when operations fail. Exception safety is commonly discussed through three guarantees. The no-throw guarantee promises that an operation will not throw. The strong guarantee promises that if an operation fails, the program state remains unchanged. The basic guarantee promises that invariants remain valid and no resources leak, although state may have changed. RAII supplies an essential foundation for these guarantees, but it does not automatically make every operation strongly exception-safe. For example, an assignment operation that modifies several members may leave a partially updated object if a later allocation fails. A common strategy is copy-and-swap: make a safe copy first, then exchange the copy with the current object. If copying fails, the original remains intact. A transaction object can apply the same reasoning to external systems. Its constructor may begin a transaction, its `commit()` function may mark success, and its destructor may roll back unless commitment has occurred. Such a design should account carefully for exceptions, thread cancellation, and the possibility that rollback itself can fail. Lifetime Hazards in Modern C++The most dangerous lifetime defects often compile successfully. A dangling pointer refers to storage whose object has already been destroyed. Returning a pointer or reference to a local variable is a common example. Storing a pointer into a `std::vector` and then triggering reallocation can also invalidate the pointer, because elements may move to new storage. A use-after-free occurs when a program accesses an object after its lifetime ends. A double deletion occurs when the same resource is released more than once. A leak occurs when an acquired resource is no longer reachable by an appropriate owner. A reference cycle involving `shared_ptr` keeps objects alive indefinitely. These defects are forms of undefined behavior or resource failure, not ordinary recoverable errors. Temporary lifetime also requires attention. A reference may extend the lifetime of a temporary in some contexts, but not universally. Binding a reference member or returning a reference to a temporary can create subtle defects. C++20 and later provide tools such as `std::span` and ranges, but these are non-owning views; their validity still depends on the lifetime of the underlying storage. Concurrency introduces additional hazards. A `shared_ptr` protects the lifetime of its managed object, but it does not automatically make simultaneous access to the object’s data thread-safe. Lifetime safety and data-race safety are separate concerns. Synchronization must protect shared mutable state, and an observer must ensure that the observed object remains alive for the entire operation. An Illustrative Case Study: Refactoring a Network Session ManagerBackgroundAssume an illustrative software company maintains a server that handles client sessions. An old session manager stores sockets as raw pointers and creates a worker object for each connection using `new`. Cleanup is attempted in several error-handling branches. Situation and problemUnder heavy load, monitoring reveals increasing memory consumption and a growing number of open sockets. Some failures occur only when authentication fails midway through a connection. A further defect appears when a session is removed from a container while a logging callback still holds a pointer to it. The underlying design has three weaknesses. First, ownership is not documented. Second, cleanup is distributed across control-flow branches. Third, callbacks use non-owning pointers without a clear lifetime contract. AnalysisThe session manager, rather than the callback, should own each active session. A session should contain its socket through an RAII wrapper. The collection should hold `unique_ptr<Session>` if sessions require stable dynamic identity. A callback that merely reports an event should receive a reference during synchronous execution, or a carefully controlled `weak_ptr` if asynchronous observation is required. If the worker and manager own one another through `shared_ptr`, a cycle may result. The manager should therefore own workers strongly, while a worker’s back-reference to the manager should be non-owning or weak. This expresses the intended topology rather than hiding it behind universal shared ownership. Approach and solutionThe redesigned architecture uses the following policy: - The session manager stores `std::unordered_map<SessionId, std::unique_ptr<Session>>`.
- Each session owns its socket through a custom RAII handle.
- Authentication functions return errors or throw exceptions, but no cleanup branch manually deletes a session.
- Synchronous callbacks receive references whose validity is limited to the call.
- Long-lived asynchronous tasks store `weak_ptr` only when shared ownership is genuinely needed.
- The session destructor closes the socket automatically.
- Shutdown first prevents new work, then destroys owning containers, allowing deterministic cleanup.
OutcomeThe redesign removes manual deletion from ordinary paths, ensures that failed authentication releases the socket during stack unwinding, and makes ownership visible in the types. The callback contract becomes explicit, reducing use-after-free risk. Resource usage becomes easier to reason about because the lifetime of a session is bounded by its owning container. Learning pointsThe case demonstrates that resource management is primarily an architectural concern. Smart pointers are useful, but they do not replace ownership analysis. `unique_ptr` communicates exclusive ownership; `weak_ptr` can express observation without prolonging lifetime; and RAII makes failure paths equivalent to normal scope exit. The most important improvement is not a particular pointer type but a clear lifetime model. Professional and Technical ConsiderationsIn production software, resource management affects reliability, security, performance, and operational cost. A leaked file descriptor can eventually prevent a service from accepting new connections. A stale authorization object can expose data if lifetime and revocation are not coordinated. A lock held longer than intended can create contention or deadlock. A destructor that performs unpredictable blocking I/O may make latency difficult to control. Teams should establish ownership conventions in coding standards and code reviews. Interfaces should communicate whether a function borrows, consumes, shares, or returns ownership. Static analysis, compiler warnings, sanitizers, and code-review checklists are valuable complements to language features. AddressSanitizer can detect many use-after-free and buffer errors; LeakSanitizer can help identify leaks; ThreadSanitizer can expose data races. These tools do not prove correctness, but they reveal defects that ordinary testing may miss. Resource release can also have security implications. Sensitive buffers may require explicit cleansing, subject to compiler-optimization concerns. File and socket handles may need privilege-aware closure. Temporary files should be created and removed safely. Destructors must not assume that unrelated global objects are still alive during program shutdown, so global initialization and destruction order should be designed cautiously. Current Directions and Best PracticesModern C++ development increasingly favours value semantics, standard library containers, views, and RAII wrappers over manual allocation. Allocator-aware containers, polymorphic memory resources, coroutines, asynchronous operations, and modules introduce additional lifetime questions, but the underlying principles remain unchanged: identify the owner, establish valid use intervals, and make release deterministic. Coroutines deserve particular care because local variables may survive suspension and resume points. An asynchronous operation must ensure that referenced objects outlive the coroutine’s use of them. Capturing a raw `this` pointer in a delayed callback is safe only when the enclosing object’s lifetime is guaranteed. Otherwise, an ownership or cancellation protocol is required. The following practices provide a practical standard for professional C++: - Prefer automatic objects and standard containers over direct dynamic allocation.
- Use `make_unique` and `make_shared` rather than naked `new` in ordinary code.
- Default to `unique_ptr` when dynamic ownership is needed.
- Use `shared_ptr` only when shared lifetime is part of the design.
- Use references, views, or raw pointers for clearly documented non-owning relationships.
- Make polymorphic base destructors virtual when deletion through the base is possible.
- Follow the Rule of Zero whenever feasible.
- Keep destructors non-throwing and focused on reliable release.
- Document moved-from states when they matter to users of a type.
- Test exceptional paths, shutdown paths, cancellation, and container invalidation.
- Treat lifetime safety and thread safety as separate design obligations.
Chapter SummaryObject lifetime is the interval in which a constructed object may legally be used, while storage duration describes how long its storage exists and ownership identifies responsibility for a resource. Constructors establish invariants and destructors release resources; their order follows well-defined rules involving bases and members. RAII makes resource release deterministic by binding acquisition and cleanup to object lifetime. Smart pointers communicate different ownership models. `unique_ptr` expresses exclusive ownership, `shared_ptr` shared ownership, and `weak_ptr` non-owning observation of a shared control block. Raw pointers and references may be appropriate for borrowing, but their lifetime contracts must be explicit. Copying a resource-owning object requires deep-copy semantics or another deliberate policy, while moving transfers ownership efficiently. The Rule of Zero is preferred, with the Rule of Three and Rule of Five applicable when custom resource management is unavoidable. Exception safety, concurrency, asynchronous execution, and shutdown all require explicit lifetime reasoning. Professional C++ design therefore combines language rules, standard-library abstractions, ownership documentation, testing, static analysis, and architectural discipline. Key TermsStorage duration: The category describing how long storage for an object exists: automatic, dynamic, static, or thread. Object lifetime: The interval between completion of object initialization and the beginning of its destruction or another lifetime-ending event. Ownership: Responsibility for controlling and releasing a resource. RAII: Resource Acquisition Is Initialization; a design pattern that binds resource ownership to an object’s lifetime. Constructor: A member function responsible for establishing a new object’s initial valid state. Destructor: A member function invoked when an object’s lifetime ends to release owned resources. Automatic storage duration: Storage associated with scope-based objects that is normally released on leaving the scope. Dynamic storage duration: Storage obtained during execution and released through an ownership mechanism. Dangling pointer: A pointer whose target object has already ceased to exist. Use-after-free: Access to an object after its lifetime has ended. Deep copy: Copying that creates an independent resource or representation for the destination. Move semantics: Operations that transfer resources from one object to another. `unique_ptr`: A smart pointer representing exclusive ownership. `shared_ptr`: A smart pointer representing reference-counted shared ownership. `weak_ptr`: A non-owning observer associated with a `shared_ptr` control block. Rule of Zero: The design guideline of relying on resource-managing members so no special member functions are required. Rule of Five: The guideline that custom resource-managing types may need to define destructor, copy constructor, copy assignment, move constructor, and move assignment. Exception safety: The degree to which an operation preserves invariants and resource correctness when failure occurs. Resource leak: Failure to release an acquired resource after it is no longer usable. Reference cycle: A closed chain of strong ownership, often through `shared_ptr`, that prevents reclamation. Review QuestionsShort Answer Questions1. Distinguish storage duration from object lifetime. 2. Why should a polymorphic base class often have a virtual destructor? 3. What resource-management problem does RAII address? 4. How does `weak_ptr` differ from `shared_ptr`? 5. State the main idea of the Rule of Zero. Long Answer Questions1. Explain the order of construction and destruction for base classes, members, and derived objects, using a suitable example. 2. Compare `unique_ptr`, `shared_ptr`, `weak_ptr`, references, and raw pointers as ownership or observation mechanisms. 3. Explain the Rule of Three, Rule of Five, and Rule of Zero and describe when each is relevant. 4. Discuss how RAII contributes to exception safety in a system that manages files, locks, and dynamic memory. 5. Analyse the causes and prevention of dangling pointers, double deletion, leaks, and reference cycles. Analytical / Application Questions1. A class stores a raw file handle and is copied by value in a container. Identify the likely lifetime defects and redesign the class using RAII. 2. A callback stores `this` and executes several seconds after the initiating object may have been destroyed. Propose two safe designs and explain their trade-offs. 3. A graph model uses `shared_ptr` for both parent-to-child and child-to-parent links. Explain why objects may leak and design an ownership structure that avoids the cycle. 22. Multiple Choice Questions with AnswersQuestion 1A local pointer variable points to an object allocated with `new`. What happens when the pointer's scope ends? A. The dynamically allocated object is automatically destroyed. B. The pointer is destroyed, but the dynamically allocated object remains unless an owner releases it. C. Both pointer and object acquire static storage duration. D. The object is automatically converted into a `shared_ptr`. Correct Answer: B. The pointer is destroyed, but the dynamically allocated object remains unless an owner releases it. Question 2Which statement best describes RAII? A. Resources are acquired globally and released at program termination. B. Resources are acquired during construction and released during destruction. C. Resources are released only when an exception is thrown. D. Resources are managed exclusively through garbage collection. Correct Answer: B. Resources are acquired during construction and released during destruction. Question 3A class contains a `std::unique_ptr<int>`. What is the normal implication for copying the class? A. Copying duplicates the managed integer automatically. B. Copying transfers ownership from source to destination. C. Copying is disabled unless the class defines an appropriate deep-copy operation. D. Copying causes both objects to share the same control block. Correct Answer: C. Copying is disabled unless the class defines an appropriate deep-copy operation. Question 4Why is a virtual destructor important in a polymorphic base class? A. It prevents all derived classes from being constructed. B. It ensures the derived destructor is invoked when deleting through a base pointer. C. It makes every member function virtual. D. It converts raw pointers into smart pointers. Correct Answer: B. It ensures the derived destructor is invoked when deleting through a base pointer. Question 5Which ownership model is most appropriate when exactly one object should control the lifetime of a dynamically allocated resource? A. `std::unique_ptr` B. `std::shared_ptr` C. `std::weak_ptr` D. A non-owning reference Correct Answer: A. `std::unique_ptr` Question 6What is the primary purpose of `std::weak_ptr`? A. To create a second owning reference count. B. To observe an object managed by `shared_ptr` without extending its lifetime. C. To transfer exclusive ownership from a `unique_ptr`. D. To prevent all destruction of the observed object. Correct Answer: B. To observe an object managed by `shared_ptr` without extending its lifetime. Question 7A class explicitly defines a destructor, copy constructor, and copy assignment operator because it owns a raw resource. Which guideline indicates that move operations may also need consideration? A. Rule of Zero B. Rule of Five C. Principle of least privilege D. Liskov substitution principle Correct Answer: B. Rule of Five Question 8Which event commonly invalidates pointers to elements of a vector? A. Reading an element through a const reference B. Vector reallocation caused by growth beyond its capacity C. Calling `size()` D. Moving a scalar integer into another variable Correct Answer: B. Vector reallocation caused by growth beyond its capacity Question 9Two objects own `shared_ptr`s that refer to each other. What is the likely result? A. The objects are always destroyed immediately. B. The reference counts may never reach zero, causing a cycle and a leak. C. The compiler automatically replaces one pointer with a `weak_ptr`. D. The objects become stack objects. Correct Answer: B. The reference counts may never reach zero, causing a cycle and a leak. Question 10Which design most closely follows the Rule of Zero? A. A class manually calls `delete` in a handwritten destructor. B. A class stores a raw handle and defines all five special member functions. C. A class composes standard resource-managing members and defines no special member functions. D. A class disables construction and destruction. Correct Answer: C. A class composes standard resource-managing members and defines no special member functions.
|