Object-Oriented Programming (C++)

Chapter 4
Object Lifetime, Value Semantics and RAII

Bachelors Program in Computer Science Engineering
Object-Oriented Programming (C++)   •   Bachelor

Object Lifetime, Value Semantics and RAII

Introduction

Object-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 Objectives

By 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 Lifetime

These 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

CategoryTypical exampleStorage existsImportant lifetime point
Automaticlocal variable inside a blockOn entering its declaration's execution until block exitInitialization starts the lifetime; destruction normally occurs on leaving the block
Staticnamespace variable or static localFor the duration of the programA 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)
Threadthread_local objectFor the duration of a threadLifetime begins and ends according to thread-local initialization and thread termination
Dynamicobject created by newFrom allocation until deallocationThe 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 lifetime

int* 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 dangles

2. Initialization and Destruction

Initialization 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 order

For an object of a derived class, destruction proceeds in the reverse of construction:

  1. The derived-class destructor body executes.
  2. The derived class's non-static data members are destroyed in reverse declaration order.
  3. The derived class's direct non-virtual base classes are destroyed in reverse construction order.
  4. Virtual base classes are destroyed in reverse construction order (at the appropriate most-derived-object stage).
  5. 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 Semantics

A 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 20

Reference 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 passing

Use 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 Functions

A class may have six important special member functions:

  1. default constructor;
  2. destructor;
  3. copy constructor;
  4. copy assignment operator;
  5. move constructor;
  6. 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 assignment

Copy 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 assignment

Moving 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 Example

Consider 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 Five

Rule of Zero

If 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 Five

If 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 Management

Resource 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 thrown

RAII 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:

RequirementTypical type
Exclusive dynamic ownershipstd::unique_ptr<T>
Shared ownership with independent lifetimestd::shared_ptr<T>
Non-owning observationraw pointer, reference, or observer type
Contiguous dynamic sequencestd::vector<T>
File stream ownershipstd::ifstream / std::ofstream
Mutex lockingstd::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 safety

RAII 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 Trace

struct 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 Guard

Background

An inventory service updates stock in a database. An update may fail because of validation, connectivity, or an exception in application code.

Problem

If every caller manually writes begin, commit, and rollback, some exceptional path may forget rollback, leaving a transaction open or partially applied.

Approach

An 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 points

The 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

  1. Deleting through a non-virtual base destructor: use a virtual destructor for polymorphic bases.
  2. Returning a pointer or reference to a local object: return by value or transfer ownership explicitly.
  3. Shallow-copying an owning raw pointer: deep-copy, prohibit copying, or replace the pointer with an RAII member.
  4. Using a moved-from value without a documented contract: restrict use to valid operations such as destruction or reassignment.
  5. Calling delete on memory not obtained with new: match allocation and deallocation mechanisms.
  6. Manually invoking a destructor unnecessarily: this ends the object's lifetime and may cause double destruction when scope ends.
  7. Using shared_ptr for every relationship: distinguish ownership from observation.
  8. Ignoring compiler warnings about initialization order: declaration order determines initialization.
  9. Writing throwing destructors: cleanup during stack unwinding can cause program termination.
  10. 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 Summary

Scope 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 Terms

Scope: 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 Questions

Short Answer Questions

  1. Distinguish scope, storage duration, and object lifetime.
  2. Why are members initialized in declaration order rather than initializer-list order?
  3. What is the difference between copy construction and copy assignment?
  4. Why should a polymorphic base usually have a virtual destructor?
  5. Explain the central idea of RAII.

Long Answer Questions

  1. Explain all storage-duration categories in C++ and clarify why dynamic storage duration must not be conflated with object lifetime.
  2. Describe the Rule of Zero and Rule of Five, including circumstances in which each is appropriate.
  3. Explain value and reference semantics with examples involving containers, pointers, and smart pointers.
  4. Describe construction and destruction order in a class hierarchy, including virtual bases.
  5. Analyze the exception-safety properties of copy-and-swap assignment.

Analytical / Application Questions

  1. Design an ownership policy for a tree structure with parent links and child nodes. Justify which links should own and which should observe.
  2. 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.
  3. 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 Answers

Question 1

Which 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 2

What 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 3

Which 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 4

What 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 5

Which 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 6

Why 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 7

Which 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 8

What 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 9

Which 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 10

In 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

Object-Oriented Programming (C++)
Chapter 4 of 15