Object Lifetime and Resource ManagementIntroductionIn a mechatronic system, software objects often represent sensors, actuators, communication channels, files, buffers, threads, and mathematical models. These objects consume resources whose correct management is essential. A memory leak may eventually stop a controller; a dangling pointer may produce an intermittent actuator command; an incorrect copy operation may duplicate ownership of a hardware handle; and an exception during initialization may leave a device in an unsafe state. C++ provides precise control over object lifetime and resource ownership. This control is powerful, but it places design responsibilities on the programmer. This chapter develops the concepts of storage duration, scope, stack and dynamic allocation, resource acquisition is initialization (RAII), copy and move semantics, assignment, smart pointers, and the rules of zero, three, and five. Learning ObjectivesBy the end of this chapter, a student should be able to: - distinguish scope, lifetime, and storage duration;
- explain automatic, static, thread, and dynamic storage duration;
- analyze stack and heap allocation and their limitations;
- apply RAII to memory and non-memory resources;
- distinguish copying from moving and implement safe special member functions;
- select suitable smart-pointer types;
- apply the rule of zero, rule of three, and rule of five;
- identify leaks, dangling references, double deletion, and ownership errors;
- design resource-safe components for mechatronic applications.
1. Scope, Lifetime, and Storage DurationThese terms are related but not interchangeable. Scope is the region of source code in which a name can be used. A local variable declared inside a function has block scope. A class member is accessible according to class access rules, and a namespace-level name may have namespace scope. Lifetime is the interval during program execution during which an object exists. It begins after initialization is complete and ends when destruction is complete. Storage duration describes how long the storage occupied by an object is reserved. C++ commonly distinguishes four categories: | Storage duration | Typical example | Begins | Ends |
|---|
| Automatic | Local variable | On entering its block, subject to initialization | On leaving its block | | Static | Global or static object | Before program use, with initialization rules | Program termination | | Thread | thread_local object | For each thread | Thread termination | | Dynamic | Object created by new | Successful allocation and construction | delete, a smart pointer, or another owner |
An object’s scope can end before its lifetime. For example, an object may be created dynamically through a local pointer; the pointer’s scope ends when the function returns, but the dynamically allocated object remains alive if another owner exists. Conversely, storage may be reserved without a usable object being alive in it; object lifetime rules determine when an object has actually begun and ended. Block Scope and Destruction OrderAutomatic objects are destroyed in the reverse order of completed construction. This supports safe dependency management. If object B depends on object A, declaring A before B generally causes B to be destroyed first. void controlCycle() {
Logger logger;
MotorDriver motor(logger);
// motor is destroyed first; logger is destroyed second
}The principle applies to data members as well: members are destroyed in reverse order of their declaration, not the order in a constructor’s initializer list. Therefore, members should normally be declared in dependency order. 2. Automatic and Dynamic AllocationAn automatic object is usually implemented in a function-call stack, although the C++ standard specifies behavior rather than requiring a particular physical stack. It has predictable destruction and low management overhead. void sample() {
SensorReading reading{}; // automatic lifetime
std::array<int, 16> samples{}; // automatic storage
}Dynamic allocation obtains storage during execution. The expression new T(arguments) allocates storage and constructs a T; delete destroys the object and releases its storage. int* p = new int(42);
delete p;
p = nullptr; Raw new and delete are sometimes necessary at low-level interfaces, but they are error-prone. Every successful allocation must have a matching release, and the release must occur exactly once. Exceptions and multiple return paths make manual matching difficult. Dynamic allocation is appropriate when: - an object must outlive the block that created it;
- its size is determined at run time;
- polymorphic objects are created through a base-class interface;
- a resource has a naturally shared or separately controlled lifetime.
It is not automatically better than automatic storage. Dynamic allocation can add latency, fragmentation, synchronization overhead, and failure modes. In real-time control, deterministic allocation policies are often preferred: fixed-size buffers, object pools, or preallocation during system startup. Common ErrorsA memory leak occurs when allocated storage becomes unreachable without being released. A dangling pointer refers to an object whose lifetime has ended. A double deletion releases the same allocation twice. A use-after-free accesses an object after destruction. These are undefined behavior and can corrupt unrelated parts of a system. int* bad() {
int value = 10;
return &value; // dangling pointer after return
}The safe alternative is to return by value, return a reference only when the referred object outlives the caller, or return an owning smart pointer when dynamic lifetime is required. 3. RAII: Resource Acquisition Is InitializationRAII binds resource ownership to an object’s lifetime. Acquisition occurs during construction; release occurs during destruction. Since destructors run automatically when control leaves a scope—including during exception unwinding—RAII makes cleanup reliable. RAII applies to: - dynamic memory;
- file descriptors and files;
- serial ports and network sockets;
- mutex locks;
- hardware handles;
- transactions and temporary device states.
The standard library illustrates the pattern: {
std::lock_guard<std::mutex> lock(mutex);
// protected operation
} // lock is released automaticallyA useful RAII wrapper has a clear invariant: either it owns a valid resource or it owns no resource. Its destructor releases the resource and should normally not throw exceptions. class FileHandle {
public:
explicit FileHandle(const char* name) : file_(std::fopen(name, "r")) {}
~FileHandle() {
if (file_) std::fclose(file_);
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
bool valid() const noexcept { return file_ != nullptr; }
private:
std::FILE* file_{};
};This class has unique ownership: copying is disabled because two objects must not close the same file handle. A production wrapper should also define or report an acquisition failure policy, such as throwing an exception or exposing an error state. 4. Copy SemanticsCopy construction creates a new object from an existing object. Copy assignment replaces the state of an existing object with a copy of another object. Motor a;
Motor b = a; // copy construction
Motor c;
c = a; // copy assignment For value-like classes, copying should produce independent, equivalent values. For a class containing a raw owning pointer, the compiler-generated copy operations perform shallow copying: both objects point to the same resource. That often causes double deletion or unintended shared mutation. A deep copy allocates a separate resource and copies its contents. A shallow copy copies an address or handle without creating an independent resource. Neither is universally correct; the ownership model must decide. Copy assignment must also handle self-assignment safely and must preserve class invariants if copying fails. The copy-and-swap idiom is one possible strategy: Buffer& Buffer::operator=(Buffer other) {
swap(*this, other);
return *this;
}The parameter is copied first. The swap then gives the target the copied state, while the temporary releases the old state when it is destroyed. This can provide strong exception safety if copying and swapping are correctly implemented. 5. Move SemanticsMove construction transfers resources from a source object rather than duplicating them. Move assignment does the same for an already existing target. They normally accept an rvalue reference, written T&&. std::vector<int> makeSamples() {
std::vector<int> result(1000);
return result; // move or elision; no expensive logical copy is required
}After a move, the source object remains valid but its value is generally unspecified. It may be assigned to, destroyed, or reset, but application code should not assume it retains its former contents. A movable resource-owning type commonly transfers a pointer and sets the source pointer to nullptr: class Block {
public:
explicit Block(std::size_t n) : data_(new int[n]), size_(n) {}
~Block() { delete[] data_; }
Block(const Block&) = delete;
Block& operator=(const Block&) = delete;
Block(Block&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
Block& operator=(Block&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
private:
int* data_{};
std::size_t size_{};
};The noexcept specification is important for many standard containers. A vector may prefer moving elements during reallocation only when moving is known not to throw; otherwise it may copy them to preserve exception guarantees. 6. The Rule of Zero, Three, and FiveThe rule of zero is the preferred design: if a class’s members manage resources through RAII types such as std::vector, std::string, and std::unique_ptr, the class should usually declare none of the destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator. class Controller {
private:
std::vector<double> gains_;
std::unique_ptr<Logger> logger_;
};The member types already provide correct lifetime behavior. The rule of three states that if a class needs to declare any one of the following, it probably needs all three: - destructor;
- copy constructor;
- copy assignment operator.
This is a warning sign that the class directly manages a resource requiring special copying. The rule of five extends the rule of three by adding: - move constructor;
- move assignment operator.
A class that manually owns a resource and supports efficient transfers should consider all five. However, declaring a destructor can suppress implicit move operations, so move support may need to be explicitly defaulted or defined. These are design guidelines, not a requirement to implement five functions mechanically. If copying is conceptually invalid, explicitly delete copy operations and implement only the valid ownership operations. 7. Smart Pointers and Ownership ModelsSmart pointers are objects that express ownership and perform automatic cleanup. | Type | Ownership model | Typical use |
|---|
std::unique_ptr<T> | Exclusive | One owner of a dynamically allocated object | std::shared_ptr<T> | Shared, reference-counted | Multiple independent owners | std::weak_ptr<T> | Non-owning observation | Breaking cycles or observing shared objects |
std::unique_ptr
Use unique_ptr by default when dynamic allocation is necessary and there is one owner. auto driver = std::make_unique<MotorDriver>();
std::unique_ptr<MotorDriver> backup = std::move(driver); After the move, driver is empty and backup owns the driver. A unique_ptr can be passed by reference for observation, moved to transfer ownership, or replaced by a raw pointer/reference for a non-owning operation. std::shared_ptr
shared_ptr uses a control block that typically stores a reference count and deletion information. The managed object is destroyed when the last owning shared_ptr is destroyed or reset.
auto model = std::make_shared<CalibrationModel>();
process(model); // another owner may be created Reference counting does not make an object automatically thread-safe, and shared ownership can obscure responsibility and prolong lifetime. Prefer unique_ptr unless shared ownership is genuinely required. std::weak_ptr
A weak_ptr does not keep the object alive. To use the object safely, call lock() and test the resulting shared_ptr: if (auto current = weakModel.lock()) {
current->update();
}It is especially useful for observer relationships and for breaking cycles such as parent–child graphs where a child observes its parent. Custom DeletersSome resources are not released with delete. A smart pointer can store a custom deleter: using FilePtr = std::unique_ptr<std::FILE, int(*)(std::FILE*)>;
FilePtr file(std::fopen("data.bin", "rb"), std::fclose);The deleter must be compatible with the resource and must not release it more than once. 8. Assignment and Exception SafetyAssignment differs from initialization: the target already owns a state that must be released or replaced. A sound assignment operation maintains class invariants even if an operation fails. The main exception-safety guarantees are: - No-throw guarantee: the operation cannot fail by throwing;
- Strong guarantee: failure leaves the program state unchanged;
- Basic guarantee: invariants hold and no resources leak, but state may change;
- No guarantee: generally unacceptable for resource-management code.
Move operations are often marked noexcept when they only transfer ownership and cannot fail. Destructors should not throw; an exception escaping a destructor during stack unwinding can terminate the program. 9. Applied Case: A Robotic Gripper ControllerBackgroundConsider an illustrative robotic gripper controller. It communicates with a motor driver through a file descriptor and stores calibration data. The controller is created during system initialization and may be replaced when a new tool head is attached. ProblemAn initial implementation stores a raw descriptor and a raw pointer to calibration data. Its default copy operations duplicate the descriptor and pointer. When a temporary controller is copied and destroyed, the live controller may use a closed descriptor or freed calibration block. AnalysisThe descriptor is an exclusive resource. Calibration data is value-like and should be copied independently, or managed by a standard container. The controller should have one clear owner of the descriptor, deterministic release, and safe replacement semantics. Approach and SolutionA robust design uses an RAII descriptor wrapper and std::vector<double> for calibration values: class GripperController {
public:
GripperController(DeviceHandle handle,
std::vector<double> calibration)
: handle_(handle), calibration_(std::move(calibration)) {}
GripperController(const GripperController&) = delete;
GripperController& operator=(const GripperController&) = delete;
GripperController(GripperController&&) noexcept = default;
GripperController& operator=(GripperController&&) noexcept = default;
private:
DeviceHandle handle_; // RAII exclusive owner
std::vector<double> calibration_; // value semantics
};The class cannot be accidentally copied, can be transferred efficiently, and delegates cleanup to member types. If multiple software components must access the same controller, shared ownership should be introduced only after defining synchronization and shutdown policies. OutcomeThe design removes manual cleanup from normal control flow, prevents double release, clarifies ownership, and makes replacement of a controller explicit through move assignment. Learning Points- Hardware handles are resources even though they are not memory.
- RAII makes cleanup occur on normal return and exception paths.
- Ownership policy should be visible in the type system.
- Rule-of-zero design is usually safer than hand-written special members.
- Real-time systems should also assess allocation timing and fragmentation.
10. Best Practices and LimitationsUse automatic objects and standard containers whenever possible. Prefer make_unique and make_shared over explicit new. Do not return pointers or references to local objects. Avoid owning raw pointers; if a raw pointer is used, document that it is non-owning. Do not create multiple owning smart pointers from the same raw pointer. Use weak_ptr for non-owning observation of shared objects. Keep ownership graphs simple, and test destruction, replacement, exception paths, and moved-from states. Resource safety does not solve every systems problem. A correctly managed object may still control a device incorrectly. Shared ownership may introduce nondeterministic destruction, and dynamic allocation may be unacceptable in hard real-time loops. Safety-critical applications require additional standards, code review, static analysis, testing, and explicit fault-handling policies. In mechatronics, software must also place hardware in a safe state when a resource or communication channel fails. Chapter SummaryObject scope identifies where a name is usable, while lifetime identifies when an object exists. Storage duration may be automatic, static, thread, or dynamic. Automatic storage provides convenient deterministic destruction; dynamic storage supports run-time lifetime and polymorphism but increases management risks. RAII connects resource ownership to object lifetime and is the central C++ technique for reliable cleanup. Copying should be used for independent value semantics or deliberately defined shared semantics. Moving transfers resources efficiently while leaving the source valid but unspecified. The rule of zero favors resource-managing member types; the rules of three and five guide classes that directly manage resources. unique_ptr expresses exclusive ownership, shared_ptr shared ownership, and weak_ptr non-owning observation. Safe assignment, non-throwing destructors, explicit ownership, and attention to real-time constraints are essential in professional mechatronic software.
Key TermsScope: Region of source code where a name can be used. Lifetime: Period during which an object exists. Storage duration: Classification of how long an object’s storage is reserved. Automatic storage: Storage associated with block execution and automatic destruction. Dynamic storage: Storage obtained during execution, commonly through allocation. RAII: Design technique that acquires a resource in construction and releases it in destruction. Resource: An entity requiring controlled acquisition and release. Ownership: Responsibility for managing a resource’s lifetime. Dangling pointer: Pointer referring to an object whose lifetime has ended. Memory leak: Allocated storage that is no longer reachable and has not been released. Deep copy: Copy creating an independent resource or resource state. Shallow copy: Copying an address or handle without independent ownership. Move construction: Construction by transferring resources from another object. Move assignment: Replacement of a target’s state by transferred resources. Rule of zero: Prefer declaring no special member functions by using RAII members. Rule of three: Guidance to define destructor, copy constructor, and copy assignment together. Rule of five: Extension of the rule of three to include move constructor and move assignment. unique_ptr: Smart pointer expressing exclusive ownership.
shared_ptr: Smart pointer supporting reference-counted shared ownership.
weak_ptr: Non-owning observer associated with a shared_ptr control block.
Review Questions5 Short Answer Questions- Distinguish scope from lifetime.
- Name the four principal C++ storage-duration categories.
- What problem does RAII solve?
- What is the state of a moved-from object expected to be?
- Why is
unique_ptr generally preferred over shared_ptr when ownership is exclusive?
5 Long Answer Questions- Explain automatic and dynamic storage, including their advantages and risks in a mechatronic controller.
- Describe shallow copying, deep copying, and the double-deletion problem.
- Explain the rule of zero, rule of three, and rule of five, giving a suitable use case for each.
- Compare
unique_ptr, shared_ptr, and weak_ptr in terms of ownership and application. - Explain how exception safety influences assignment operators and destructors.
3 Analytical / Application Questions- A class contains a raw pointer to a motor-driver handle and uses the compiler-generated copy constructor. Analyze the possible failures and redesign the ownership model.
- A real-time control loop allocates and deletes a sensor buffer on every cycle. Evaluate the risks and propose two safer alternatives.
- Design the ownership relationships for a robot model containing a controller, several sensors, and observer objects. Justify where exclusive ownership, shared ownership, and non-owning observation should be used.
Multiple Choice Questions with AnswersQuestion 1Which statement best distinguishes scope from lifetime? A. Scope describes where a name is usable; lifetime describes when the object exists. B. Scope describes memory size; lifetime describes execution speed. C. Scope and lifetime are always identical. D. Scope applies only to dynamic objects. Correct Answer: A. Scope describes where a name is usable; lifetime describes when the object exists. Question 2Which technique binds resource release to object destruction? A. Template specialization B. RAII C. Function overloading D. Namespace qualification Correct Answer: B. RAII Question 3What is the usual ownership meaning of std::unique_ptr<T>? A. Non-owning observation B. Reference-counted shared ownership C. Exclusive ownership D. Temporary stack ownership Correct Answer: C. Exclusive ownership Question 4What should a moved-from object generally be assumed to be? A. Invalid and impossible to destroy B. Valid but with an unspecified value C. Automatically copied back to the source D. Guaranteed to contain its original value Correct Answer: B. Valid but with an unspecified value Question 5Which set belongs to the rule of three? A. Constructor, default constructor, and conversion operator B. Destructor, copy constructor, and copy assignment operator C. Move constructor, move assignment, and destructor only D. Virtual function, friend function, and destructor Correct Answer: B. Destructor, copy constructor, and copy assignment operator Question 6Why can compiler-generated copying be dangerous for a class owning a raw pointer? A. It always prevents compilation. B. It performs a shallow copy, potentially leaving two owners of one resource. C. It converts the pointer into a reference. D. It automatically creates a shared_ptr. Correct Answer: B. It performs a shallow copy, potentially leaving two owners of one resource. Question 7Which pointer type is designed to observe a shared_ptr-managed object without extending its lifetime? A. std::unique_ptr B. std::weak_ptr C. Raw owning pointer D. std::array Correct Answer: B. std::weak_ptr Question 8What is the preferred design when a class can rely entirely on RAII member types for resource management? A. Declare all five special member functions manually. B. Use the rule of zero. C. Disable the destructor. D. Allocate every member with raw new. Correct Answer: B. Use the rule of zero. Question 9Which operation transfers ownership from an existing unique_ptr to another? A. Copy construction B. std::move C. Address-of operator D. Reference binding to a const integer Correct Answer: B. std::move Question 10Why are destructors normally required not to throw? A. Throwing destructors use no memory. B. An exception during stack unwinding can cause program termination. C. Destructors cannot access data members. D. The compiler replaces them with copy constructors. Correct Answer: B. An exception during stack unwinding can cause program termination.
|