# File 3: Memory Management and Modern C++

## Learning Goals

By the end of this file, you should be able to:

- describe ownership clearly instead of saying “the pointer points there” and stopping
- choose between raw pointers, references, and smart pointers based on lifetime semantics
- explain copy vs move semantics with both intuition and internal mechanics
- apply the Rule of 0, Rule of 3, and Rule of 5 in real code
- design resource-managing types that behave predictably under exceptions and refactoring

This file builds on File 1 and File 2. Once you understand lifetime, construction, and destruction, modern C++ memory management becomes a set of ownership patterns rather than a pile of features.

## Why Modern C++ Changed Memory Management Style

### Intuition

Older C++ code often used raw `new` and `delete` directly. That approach exposes too much manual lifetime bookkeeping to everyday code.

Modern C++ tries to encode ownership in types so the compiler and API design help enforce the intended lifetime model.

The goal is not to hide memory. It is to make ownership explicit and failure-resistant.

### Ownership Vocabulary

Before discussing smart pointers, use precise terms:

- owning handle: responsible for cleanup
- non-owning handle: can access an object but does not control its lifetime
- exclusive ownership: exactly one owner at a time
- shared ownership: multiple owners coordinate lifetime
- observing reference: can see an object if it still exists, but does not keep it alive

This vocabulary matters in interviews and code reviews because “it works” is not enough. Engineers need to know who frees the resource and when.

## Raw Pointers Revisited

### Intuition

A raw pointer is best treated as a non-owning access mechanism unless documentation says otherwise.

Why this shift matters:

- a raw pointer by itself does not communicate ownership clearly
- codebases that treat raw pointers as owning create leaks and double frees
- most modern APIs reserve raw pointers for nullable or borrowed access

### Good Modern Interpretation

Use raw pointers when you need one of these semantics:

- optional access to an object
- traversal without ownership transfer
- interoperability with C APIs or low-level subsystems
- custom memory systems where ownership is expressed elsewhere

### Pitfall

The problem is not that raw pointers are inherently bad. The problem is that ownership encoded only in comments is fragile.

## `std::unique_ptr`

### Intuition

`std::unique_ptr` represents exclusive ownership. One object owns the resource, and when that owner dies, the resource is released.

This is the closest high-level replacement for raw owning pointers.

### Example

```cpp
auto socket = std::make_unique<Socket>(config);

if (!socket->connect()) {
    return false;
}
```

No manual `delete` is needed. Cleanup happens automatically.

### Internal Mechanics

A `unique_ptr<T>` usually contains:

- a raw pointer to `T`
- optionally a deleter object

It is move-only, not copyable. That restriction is the entire point. The type system prevents accidental duplicate ownership.

### Why It Exists

Exclusive ownership is extremely common:

- a service owns a cache
- a tree node owns its children
- a parser owns a token buffer
- a component owns a resource handle

`unique_ptr` makes that ownership explicit and exception-safe.

### Practical Usage

- return heap objects from factories
- store polymorphic objects in containers
- model tree and DAG ownership where one parent clearly owns one child

### Pitfalls

- copying is not allowed, so design APIs around moving or referencing
- do not wrap stack objects in `unique_ptr`
- avoid calling `release()` unless you are deliberately transferring responsibility

## `std::shared_ptr`

### Intuition

`std::shared_ptr` represents shared ownership. The object stays alive until the last owning `shared_ptr` goes away.

It exists for cases where a single clear owner does not exist.

### Example

```cpp
auto session = std::make_shared<Session>(config);
worker_pool.add(session);
monitor.attach(session);
```

Both the worker pool and monitor may extend the lifetime of the same session object.

### Internal Mechanics

`shared_ptr` typically uses a control block containing:

- the reference count for strong owners
- the reference count for weak observers
- deleter and allocator information

```mermaid
flowchart LR
    A[shared_ptr A] --> C[Control block]
    B[shared_ptr B] --> C
    C --> D[Managed object]
    C --> E[strong count]
    C --> F[weak count]
```

When the strong count reaches zero, the managed object is destroyed. The control block itself can remain until weak references are gone.

### Practical Usage

- asynchronous workflows where several components may need to keep work alive
- graph-like application objects when ownership is genuinely shared
- callback systems where tasks may outlive the originating scope

### Tradeoffs

- more memory overhead than `unique_ptr`
- reference counting operations add runtime cost
- shared ownership can make program structure harder to reason about

### Common Pitfalls

- using `shared_ptr` by default instead of designing clear ownership
- creating hidden lifetime extension that makes cleanup unpredictable
- forming cycles that prevent destruction

## `std::weak_ptr`

### Intuition

`weak_ptr` exists because sometimes you need to observe a shared object without keeping it alive.

The classic use case is breaking reference cycles.

### Example of a Cycle Problem

If parent and child both store `shared_ptr` to each other, neither reference count reaches zero.

```mermaid
flowchart LR
    P[Parent shared_ptr] --> C[Child object]
    C --> W[weak_ptr back to parent]
```

With `weak_ptr`, the child can refer back to the parent without extending the parent's lifetime.

### How It Works

`weak_ptr` points to the same control block as `shared_ptr`, but it does not contribute to the strong owner count.

To use the object safely, call `lock()` to obtain a temporary `shared_ptr` if the object still exists.

```cpp
if (auto parent = weak_parent.lock()) {
    parent->notify();
}
```

### Practical Usage

- observer patterns
- caches of shared resources
- parent back-references in trees or graphs
- asynchronous callback registries

### Pitfall

Do not assume the object is still alive just because a `weak_ptr` exists. Always revalidate via `lock()`.

## Copy Semantics

### Intuition

Copying means making another object with the same logical value.

For simple types, this is straightforward. For resource-owning types, copying becomes a design decision:

- should both objects own independent resources?
- should copying be forbidden?
- should copying be expensive or cheap?

### Example

```cpp
std::string a = "trade";
std::string b = a; // copy
```

Here, `b` becomes its own string object with its own storage.

### Internal Mechanics

For resource-owning classes, a correct copy operation often requires a deep copy, not a copied raw pointer. If two objects copy the same owning raw pointer blindly, both will try to free the same resource.

That is why copy control exists at all.

## Move Semantics

### Intuition

Move semantics exist because copying expensive resources is often unnecessary. If an object is temporary or no longer needed, its resources can be transferred instead of duplicated.

This is one of the defining features of modern C++.

### Example

```cpp
std::vector<int> build_values() {
    std::vector<int> values = {1, 2, 3, 4};
    return values;
}
```

In modern C++, returning `values` is efficient because the compiler can elide copies or move the vector's internal buffer.

### Transfer Mental Model

```mermaid
flowchart LR
    A[source object owns buffer] --> B[move operation]
    B --> C[destination now owns buffer]
    B --> D[source becomes valid but unspecified]
```

### Internal Mechanics

Moves typically transfer internal pointers, handles, or buffers from one object to another and leave the source object in a valid but unspecified state.

That phrase is important:

- valid means the source can still be destroyed safely
- unspecified means you should not rely on its old value

### `std::move` Is a Cast, Not a Move by Itself

This is a common misconception.

`std::move(x)` does not move anything on its own. It casts `x` to an rvalue expression, signaling that moving is allowed if an appropriate move operation exists.

### Practical Usage

- returning large objects from functions
- transferring ownership into containers or asynchronous tasks
- avoiding unnecessary deep copies in performance-sensitive code

### Pitfalls

- using moved-from objects as though they still contain the old value
- writing move operations that forget to preserve class invariants
- overusing `std::move` on values where copy elision or normal forwarding would be better

## Rule of 3, Rule of 5, and Rule of 0

### Rule of 3

If your class manually defines any of these, it probably needs all three:

- destructor
- copy constructor
- copy assignment operator

Why:

If your class manages a resource manually, the defaults may perform shallow copies that break ownership.

### Rule of 5

In modern C++, move constructor and move assignment operator join the list.

- destructor
- copy constructor
- copy assignment
- move constructor
- move assignment

If you manage resources manually, you likely need to think about all five.

### Rule of 0

The best modern outcome is often the Rule of 0: do not manually write special member functions at all. Instead, compose your class from well-behaved members such as `std::string`, `std::vector`, `std::unique_ptr`, and other RAII types.

That lets the compiler-generated defaults behave correctly.

### Practical Guidance

- prefer Rule of 0 when possible
- use Rule of 5 only when building true resource-managing types
- if you write one special member function, stop and consider the others

## Resource Management Patterns

## Prefer RAII Wrappers Over Manual Cleanup

Wrap raw resources in types that own cleanup.

Examples:

- file descriptor wrapper
- socket wrapper
- scoped timer
- custom allocator arena handle

## Prefer Containers Over Raw Dynamic Arrays

Instead of:

```cpp
int* data = new int[count];
```

prefer:

```cpp
std::vector<int> data(count);
```

Why:

- size information stays with the data structure
- cleanup becomes automatic
- resizing and range-aware APIs become available

## Use Views for Non-Owning Access

Modern C++ increasingly uses non-owning views such as `std::string_view` and `std::span` to express borrowed access without copying.

These are powerful, but they require lifetime discipline. A view is only valid while the underlying data is alive.

### Example Pitfall

Returning `std::string_view` to a temporary `std::string` creates a dangling view.

## Exception Safety and Ownership

### Intuition

Memory management decisions matter most when control flow becomes non-linear. Exceptions, early returns, and partial initialization are exactly where manual cleanup breaks down.

RAII and smart pointers give you strong exception safety by making cleanup automatic during stack unwinding.

### Practical Levels of Safety

Common exception-safety language:

- basic guarantee: no leaks, object remains valid
- strong guarantee: operation either succeeds fully or has no observable effect
- no-throw guarantee: operation cannot throw

You do not need to recite these mechanically, but you should understand how ownership design influences them.

## Common Modern C++ Pitfalls

- using `shared_ptr` to avoid thinking about ownership
- mixing owning raw pointers with smart pointers ambiguously
- forming `shared_ptr` cycles
- assuming moved-from objects retain useful values
- exposing raw references or pointers to internal data whose lifetime is not guaranteed
- returning views to destroyed storage

## Real-World Design Examples

### Tree Ownership

Use `unique_ptr` for children and raw pointers or references for parent-aware traversal when parent does not own child separately.

### Shared Async Work

Use `shared_ptr` when multiple asynchronous callbacks must keep an object alive until all work is finished.

### C API Wrapping

Use a custom RAII wrapper or `unique_ptr` with a custom deleter for resources acquired through legacy APIs.

```cpp
using FilePtr = std::unique_ptr<std::FILE, decltype(&std::fclose)>;

FilePtr open_file(const char* path) {
    return FilePtr(std::fopen(path, "r"), &std::fclose);
}
```

## Interview Checkpoints

You should be able to explain:

- why raw pointers are weak ownership signals
- when `unique_ptr` is preferable to `shared_ptr`
- how `shared_ptr` uses a control block
- what `weak_ptr` solves
- the difference between copy and move semantics
- why `std::move` does not itself move anything
- when the Rule of 0 beats the Rule of 5

## What Comes Next

The next file focuses on the standard library, especially the containers and algorithms that most production C++ code uses every day. Many STL design choices make much more sense once you understand ownership, moves, and lifetime.