Object Lifetime, Value Semantics and RAIIIntroductionObject-oriented C++ programs do more than represent data and behavior. They also determine when objects come into existence, where their storage is obtained, how resources are transferred, and when cleanup occurs. Errors in these areas produce leaks, dangling pointers, double deletion, use-after-move, and undefined behavior. This chapter develops a systematic model of object lifetime and resource ownership. It then applies that model to value semantics, copy and move operations, destructors, the Rule of Zero/Five, and Resource Acquisition Is Initialization (RAII). Learning ObjectivesBy the end of this chapter, you should be able to: - distinguish scope, storage duration, and object lifetime;
- explain initialization and destruction order;
- compare value semantics with reference semantics;
- implement safe copy and move constructors and assignment operators;
- apply the Rule of Zero and Rule of Five;
- design RAII types for deterministic resource management;
- identify ownership errors and exception-safety risks.
1. Scope, Storage Duration and LifetimeThese terms describe different properties: - Scope is where a name can be used in source code.
- Storage duration is how long the storage associated with an object exists.
- Object lifetime is the interval during which an object has been constructed and may be used as an object of its type.
An object can have storage without yet having begun its lifetime. For example, raw allocated storage may exist before placement construction. Conversely, an object's lifetime may end before its storage is released, as occurs when a destructor is called explicitly before deallocation. Storage-duration categories| Category | Typical example | Storage exists | Important lifetime point |
|---|
| Automatic | local variable inside a block | On entering its declaration's execution until block exit | Initialization starts the lifetime; destruction normally occurs on leaving the block | | Static | namespace variable or static local | For the duration of the program | A static local's storage exists for the program, but initialization and object lifetime begin only when control first reaches its declaration (subject to initialization rules) | | Thread | thread_local object | For the duration of a thread | Lifetime begins and ends according to thread-local initialization and thread termination | | Dynamic | object created by new | From allocation until deallocation | The object's lifetime normally ends when destruction is completed; dynamic storage ends when delete or the corresponding deallocation function releases it |
Thus, “dynamic lifetime” should not be used as a synonym for storage duration. For a dynamically allocated object, destruction can end the object lifetime before the storage is returned to the allocator. Scope is not lifetimeint* make_pointer() {
int local = 42;
return &local; // wrong: local's lifetime ends at function exit
}The name local has block scope, and its object has automatic storage duration. Returning its address creates a dangling pointer because the object's lifetime ends when the function returns. A name can also leave scope while the object remains alive: int* saved;
{
auto owner = std::make_unique<int>(42);
saved = owner.get(); // owner is still alive here
} // object is destroyed; saved now dangles2. Initialization and DestructionInitialization is not optional bookkeeping. It establishes class invariants before member functions operate on an object. Prefer member-initializer lists: class Point {
int x_;
int y_;
public:
Point(int x, int y) : x_(x), y_(y) {}
};Members are initialized in their declaration order, not the order written in the constructor's initializer list. Therefore, this declaration order is authoritative: class Example {
int first_;
int second_;
public:
Example() : second_(2), first_(1) {} // initializes first_, then second_
};Compilers commonly warn when the written order differs from declaration order. Such warnings should be treated seriously. Destruction orderFor an object of a derived class, destruction proceeds in the reverse of construction: - The derived-class destructor body executes.
- The derived class's non-static data members are destroyed in reverse declaration order.
- The derived class's direct non-virtual base classes are destroyed in reverse construction order.
- Virtual base classes are destroyed in reverse construction order (at the appropriate most-derived-object stage).
- The process continues through base subobjects, with each destructor body followed by destruction of that class's members and bases.
A useful simplified rule is: the most-derived destructor starts first, and subobjects are unwound in reverse order of their construction, including virtual bases. Exact implementation details of virtual-base destruction are governed by the class hierarchy, but omitting virtual bases from the model is incorrect. A base class intended for polymorphic deletion should usually have a virtual destructor: struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};Without a virtual destructor, deleting a derived object through a base pointer is undefined behavior. 3. Value and Reference SemanticsA type has value semantics when an object behaves like an independent value. Copying it produces an independent object whose future modifications do not affect the source. Integers, std::string, and std::vector normally provide value semantics. A type has reference semantics when multiple handles designate one underlying object or resource. Pointers, references, std::shared_ptr, and observer handles may express such relationships. std::vector<int> a{1, 2, 3};
auto b = a; // independent copy
b[0] = 99; // a[0] remains 1
int x = 10;
int& ref = x; // alias, not a new integer
ref = 20; // x becomes 20Reference semantics are appropriate when identity, shared state, polymorphism, or non-owning observation is intended. They are dangerous when ownership is unclear. A raw pointer does not itself state whether it owns, observes, or participates in shared ownership. Const references and parameter passingUse const T& to avoid copying while promising not to modify the argument. Use T when a function needs its own value and may benefit from move construction. Use T& when modification of the caller's object is part of the contract. 4. Special Member FunctionsA class may have six important special member functions: - default constructor;
- destructor;
- copy constructor;
- copy assignment operator;
- move constructor;
- move assignment operator.
The compiler may implicitly declare or define some of them, but declaring a destructor, copy operation, or move operation can affect which others are generated. The exact rules are nuanced; the design principle is simpler: make ownership explicit and avoid manually managing resources when standard library types can do it. Copy construction and copy assignmentCopy construction creates a new object: T b(a);
T c = a; Copy assignment replaces the state of an existing object: c = a; A correct copy assignment operator must handle self-assignment and preserve invariants if an exception occurs. The copy-and-swap idiom is one robust approach: class Text {
std::string value_;
public:
Text(std::string value) : value_(std::move(value)) {}
Text(const Text&) = default;
Text& operator=(const Text&) = default;
};For a resource-owning class, copying generally means duplicating the resource, not merely copying an address. Move construction and move assignmentMoving transfers resources from a source object to a destination. The source remains valid but its value is generally unspecified unless the class documents a stronger postcondition. T b(std::move(a));
T c;
c = std::move(a); std::move does not move anything by itself. It converts an expression into an rvalue reference, allowing a move constructor or move assignment operator to be selected.
A moved-from standard-library object is normally valid for destruction, assignment, and operations whose preconditions it satisfies. Code should not assume that it is empty unless the type's specification guarantees that result. 5. A Resource-Owning ExampleConsider a dynamically allocated integer buffer. Its invariant is: data_ is nullptr exactly when size_ == 0, or points to an allocation containing size_ integers;- the object owns that allocation exclusively;
- destruction releases it exactly once.
#include <algorithm>
#include <cstddef>
#include <utility>
class IntBuffer {
std::size_t size_ = 0;
int* data_ = nullptr;
public:
explicit IntBuffer(std::size_t n)
: size_(n), data_(n == 0 ? nullptr : new int[n]{}) {}
~IntBuffer() {
delete[] data_;
}
IntBuffer(const IntBuffer& other)
: size_(other.size_),
data_(other.size_ == 0 ? nullptr : new int[other.size_]) {
if (size_ > 0) {
std::copy(other.data_, other.data_ + size_, data_);
}
}
IntBuffer& operator=(const IntBuffer& other) {
if (this == &other) {
return *this;
}
IntBuffer temporary(other);
swap(*this, temporary);
return *this;
}
IntBuffer(IntBuffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
IntBuffer& operator=(IntBuffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}
friend void swap(IntBuffer& a, IntBuffer& b) noexcept {
using std::swap;
swap(a.size_, b.size_);
swap(a.data_, b.data_);
}
std::size_t size() const noexcept { return size_; }
int& operator[](std::size_t i) noexcept { return data_[i]; }
const int& operator[](std::size_t i) const noexcept { return data_[i]; }
};The size_ > 0 guard is essential. When both buffers are empty, data_ and other.data_ are null. Forming other.data_ + size_ would perform pointer arithmetic on a null pointer, which is undefined behavior even when the offset is zero. The guarded implementation does not form that expression. The copy-assignment implementation first makes a copy. If allocation or copying throws, the original object is unchanged. If copying succeeds, swapping commits the new state. This gives a strong exception-safety guarantee for the assignment operation. The move operations transfer the pointer and reset the source to a harmless empty state. noexcept is important because standard containers can prefer moving over copying during reallocation when the move constructor is known not to throw. In production C++, the preferred implementation is usually: class SaferBuffer {
std::vector<int> data_;
public:
explicit SaferBuffer(std::size_t n) : data_(n) {}
// Rule of Zero: compiler-generated special members are appropriate.
};6. Rule of Zero and Rule of FiveRule of ZeroIf a class's members already manage their own resources, the class should normally declare none of the special member functions. Use std::vector, std::string, std::unique_ptr, std::fstream, locks, and other RAII types. class Report {
std::string title_;
std::vector<int> values_;
public:
Report(std::string title, std::vector<int> values)
: title_(std::move(title)), values_(std::move(values)) {}
};The compiler-generated destructor, copy operations, and move operations correctly delegate to the members. Rule of FiveIf a class directly owns a resource and must define one of the destructor, copy constructor, copy assignment, move constructor, or move assignment, it should consciously consider all five. This does not mean every function must be public or implemented identically; it means ownership semantics must be reviewed as a set. A non-copyable unique owner should explicitly disable copying: class FileHandle {
int fd_ = -1;
public:
explicit FileHandle(int fd) : fd_(fd) {}
~FileHandle();
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&&) noexcept;
FileHandle& operator=(FileHandle&&) noexcept;
};Copying a file descriptor wrapper would be incorrect unless duplication semantics were deliberately implemented. Moving transfers responsibility for closing the descriptor. 7. RAII and Deterministic Resource ManagementResource Acquisition Is Initialization binds resource ownership to object lifetime. Acquisition occurs during successful construction; release occurs in the destructor. Since C++ destroys automatic objects during normal block exit and stack unwinding, cleanup is deterministic. Resources include: - heap allocations;
- files and sockets;
- database transactions;
- mutex locks;
- operating-system handles;
- temporary configuration changes.
#include <mutex>
void update_shared_state(std::mutex& m, int& value) {
std::lock_guard<std::mutex> guard(m);
++value;
} // guard unlocks m here, including when an exception is thrownRAII is stronger than a convention such as “remember to call cleanup.” It makes cleanup part of the type's semantics. Ownership should be represented by the narrowest suitable type: | Requirement | Typical type |
|---|
| Exclusive dynamic ownership | std::unique_ptr<T> | | Shared ownership with independent lifetime | std::shared_ptr<T> | | Non-owning observation | raw pointer, reference, or observer type | | Contiguous dynamic sequence | std::vector<T> | | File stream ownership | std::ifstream / std::ofstream | | Mutex locking | std::lock_guard / std::unique_lock |
std::shared_ptr should not be used merely to avoid deciding ownership. It introduces reference-counting overhead and cannot by itself solve cycles. For cyclic graphs, use std::weak_ptr for non-owning back-references or redesign ownership.
Exception safetyRAII supports three commonly discussed guarantees: - No-throw guarantee: the operation does not throw.
- Strong guarantee: if the operation fails, observable state is unchanged.
- Basic guarantee: invariants remain valid and no resources leak, but state may change.
Move constructors and swap functions are often marked noexcept when they truly cannot fail. A false noexcept promise causes termination if an exception escapes, so it must be justified by the implementation. 8. Worked Lifetime Tracestruct Logger {
Logger(const char*) {}
~Logger() {}
};
void process() {
Logger a("a");
{
Logger b("b");
Logger c("c");
}
Logger d("d");
}Construction order is a, then b, then c, then d. Destruction order is the reverse within each enclosing scope: c, then b at the inner block's end, and finally d, then a when process exits. The inner objects are destroyed before control reaches the declaration of d's enclosing exit. This nesting property enables safe management of locks and temporary resources. 9. Applied Case: A Transaction GuardBackgroundAn inventory service updates stock in a database. An update may fail because of validation, connectivity, or an exception in application code. ProblemIf every caller manually writes begin, commit, and rollback, some exceptional path may forget rollback, leaving a transaction open or partially applied. ApproachAn illustrative RAII wrapper begins a transaction in its constructor, commits explicitly, and rolls back in its destructor unless committed: class Transaction {
Database& db_;
bool committed_ = false;
public:
explicit Transaction(Database& db) : db_(db) {
db_.begin();
}
void commit() {
db_.commit();
committed_ = true;
}
~Transaction() {
if (!committed_) {
db_.rollback();
}
}
Transaction(const Transaction&) = delete;
Transaction& operator=(const Transaction&) = delete;
};Here Database is an illustrative interface. A production implementation must specify whether rollback() can throw; destructors should generally not allow exceptions to escape. It must also define behavior if commit() partially fails and consider database-driver guarantees. Outcome and learning pointsThe guard makes rollback the default failure behavior and makes ownership non-copyable. Explicit commit represents successful completion. The design reduces duplicated cleanup code, but it does not replace transaction isolation, durability, database error handling, or testing. RAII manages the client-side protocol; it cannot guarantee external system success. 10. Common Errors and Best Practices- Deleting through a non-virtual base destructor: use a virtual destructor for polymorphic bases.
- Returning a pointer or reference to a local object: return by value or transfer ownership explicitly.
- Shallow-copying an owning raw pointer: deep-copy, prohibit copying, or replace the pointer with an RAII member.
- Using a moved-from value without a documented contract: restrict use to valid operations such as destruction or reassignment.
- Calling
delete on memory not obtained with new: match allocation and deallocation mechanisms. - Manually invoking a destructor unnecessarily: this ends the object's lifetime and may cause double destruction when scope ends.
- Using
shared_ptr for every relationship: distinguish ownership from observation. - Ignoring compiler warnings about initialization order: declaration order determines initialization.
- Writing throwing destructors: cleanup during stack unwinding can cause program termination.
- Failing to test self-assignment and exception paths: ownership code must be tested under copying, moving, empty states, and failures.
Modern C++ guidance is therefore: express ownership in types, prefer the Rule of Zero, use standard containers and smart pointers, document moved-from states, and make resource protocols exception-safe. Chapter SummaryScope identifies where names are usable; storage duration identifies how long storage exists; object lifetime identifies when a constructed object may be used. These concepts must not be conflated. Initialization establishes invariants, while destruction proceeds in reverse construction order, including members, direct bases, and virtual bases as required by the hierarchy. Value semantics provide independent copies, whereas reference semantics express identity or shared access. Resource-owning classes require carefully designed copy and move operations. The Rule of Zero favors standard RAII members; the Rule of Five requires deliberate consideration of all special member functions when direct ownership is present. RAII binds acquisition to construction and release to destruction, providing deterministic cleanup and strong support for exception safety. Key TermsScope: Region of source code in which a name can be used. Storage duration: Period during which storage associated with an object exists. Object lifetime: Period from completion of initialization until destruction or lifetime termination. Automatic storage duration: Storage commonly associated with block-local objects. Static storage duration: Storage that exists for the program's duration. Dynamic storage duration: Storage obtained from an allocation mechanism and released explicitly. Initialization: Process that establishes an object's initial state. Destructor: Special member function that releases resources and ends an object's normal lifetime. Value semantics: Behavior in which copies represent independent values. Reference semantics: Behavior in which handles designate an existing object or resource. Copy constructor: Constructor that creates an object from another object of the same type. Copy assignment: Operation that replaces an existing object's state with a copy. Move constructor: Constructor that transfers resources from a source object. Move assignment: Operation that transfers resources into an existing object. Rule of Zero: Guideline to rely on resource-managing members and declare no special members. Rule of Five: Guideline to review destructor, copy, and move operations together when ownership is direct. RAII: Resource Acquisition Is Initialization; resource ownership tied to object lifetime. Exception safety: Properties describing state and resource behavior when operations fail. Dangling pointer: Pointer that refers to an object whose lifetime has ended. Ownership: Responsibility for controlling a resource's release. Review QuestionsShort Answer Questions- Distinguish scope, storage duration, and object lifetime.
- Why are members initialized in declaration order rather than initializer-list order?
- What is the difference between copy construction and copy assignment?
- Why should a polymorphic base usually have a virtual destructor?
- Explain the central idea of RAII.
Long Answer Questions- Explain all storage-duration categories in C++ and clarify why dynamic storage duration must not be conflated with object lifetime.
- Describe the Rule of Zero and Rule of Five, including circumstances in which each is appropriate.
- Explain value and reference semantics with examples involving containers, pointers, and smart pointers.
- Describe construction and destruction order in a class hierarchy, including virtual bases.
- Analyze the exception-safety properties of copy-and-swap assignment.
Analytical / Application Questions- Design an ownership policy for a tree structure with parent links and child nodes. Justify which links should own and which should observe.
- Given a class containing a raw file handle, determine whether it should be copyable, movable, both, or neither. Explain the resource semantics and required special members.
- Review a function that manually locks a mutex, performs several operations, and unlocks at the end. Redesign it using RAII and analyze what happens if an exception occurs midway.
Multiple Choice Questions with AnswersQuestion 1Which statement correctly distinguishes storage duration from object lifetime? A. They are always identical for every C++ object. B. Storage can exist before an object's lifetime begins, and lifetime can end before storage is deallocated. C. Object lifetime always continues after storage is released. D. Only automatic objects have an object lifetime. Correct Answer: B. Storage can exist before an object's lifetime begins, and lifetime can end before storage is deallocated. Question 2What happens when an empty IntBuffer is copied in the corrected implementation? A. It forms null-pointer arithmetic and is always defined. B. It allocates one integer unnecessarily. C. It skips the copy operation because the size is zero. D. It copies from the source using memcpy with a null pointer. Correct Answer: C. It skips the copy operation because the size is zero. Question 3Which operation creates a new object from an existing object? A. Copy assignment B. Move assignment C. Copy construction D. Destruction Correct Answer: C. Copy construction Question 4What does std::move do by itself? A. It immediately transfers ownership. B. It destroys the source object. C. It converts an expression to an rvalue category that can enable moving. D. It allocates replacement storage. Correct Answer: C. It converts an expression to an rvalue category that can enable moving. Question 5Which design best follows the Rule of Zero? A. A class manually calls delete[] in its destructor. B. A class uses std::vector for its dynamic sequence and declares no special members. C. A class copies an owning raw pointer in its copy constructor. D. A class defines only a destructor and relies on shallow copying. Correct Answer: B. A class uses std::vector for its dynamic sequence and declares no special members. Question 6Why is a virtual destructor important in a polymorphic base class? A. It makes all member functions virtual. B. It permits safe destruction through a base pointer. C. It prevents stack allocation. D. It disables move construction. Correct Answer: B. It permits safe destruction through a base pointer. Question 7Which event normally releases a std::lock_guard's mutex? A. The next assignment to the mutex B. The end of the guard object's lifetime C. A call to std::move D. Copying the guard Correct Answer: B. The end of the guard object's lifetime Question 8What is the usual state guarantee for a moved-from standard-library object? A. It is always identical to its original state. B. It is always empty. C. It remains valid, but its value is generally unspecified unless documented otherwise. D. It can never be destroyed. Correct Answer: C. It remains valid, but its value is generally unspecified unless documented otherwise. Question 9Which assignment strategy can provide the strong exception-safety guarantee by making a temporary copy before swapping? A. Copy-and-swap B. Uninitialized assignment C. Pointer reinterpretation D. Destructor chaining Correct Answer: A. Copy-and-swap Question 10In reverse destruction of a most-derived object, which category must be included in a complete model? A. Only local variables in unrelated functions B. Virtual base classes where the hierarchy contains them C. Only static data members D. Only temporary expressions Correct Answer: B. Virtual base classes where the hierarchy contains them
|