# Lifetime Management

### Lifetime Management

Memory lifetime means:

```text
How long is this memory valid?
```

This is one of the most important ideas in systems programming.

A program becomes unsafe when it uses memory after that memory is no longer valid.

In Zig, you must think carefully about ownership and lifetime because the language gives you direct control over memory.

#### A Simple Lifetime

Look at this example:

```zig
const std = @import("std");

pub fn main() void {
    var numbers = [_]i32{ 1, 2, 3 };

    std.debug.print("{d}\n", .{numbers[0]});
}
```

The array `numbers` exists during the execution of `main`.

When `main` ends, the array disappears because it lives on the stack.

Its lifetime is:

```text
from entering main
until leaving main
```

Using `numbers` inside `main` is safe.

Using it after `main` ends is impossible because the program is already ending.

#### Returning Stack Memory Is Wrong

A classic lifetime bug is returning memory that belongs to a local variable.

Broken example:

```zig
const std = @import("std");

fn makeNumbers() []i32 {
    var numbers = [_]i32{ 1, 2, 3 };

    return &numbers;
}
```

This function returns a slice pointing to `numbers`.

But `numbers` is a local variable. Its lifetime ends when `makeNumbers` returns.

The returned slice points to dead stack memory.

That is a dangling slice.

The bug is conceptual:

```text
memory died
reference survived
```

That is never safe.

#### Heap Memory Can Outlive the Function

Heap allocation changes the lifetime.

```zig
const std = @import("std");

fn makeNumbers(allocator: std.mem.Allocator) ![]i32 {
    const numbers = try allocator.alloc(i32, 3);

    numbers[0] = 1;
    numbers[1] = 2;
    numbers[2] = 3;

    return numbers;
}
```

Now the memory comes from the allocator, not the stack.

The memory remains valid after the function returns.

But someone must eventually free it.

The caller becomes the owner:

```zig
const numbers = try makeNumbers(allocator);
defer allocator.free(numbers);
```

The lifetime becomes:

```text
allocate memory
return ownership to caller
caller eventually frees memory
```

#### Ownership and Lifetime

Ownership and lifetime are closely connected.

The owner of memory is responsible for its cleanup.

For example:

```zig
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
```

This scope owns `buffer`.

The lifetime of `buffer` ends when `allocator.free(buffer)` runs.

After that point, using `buffer` is invalid.

#### Use-After-Free

A use-after-free bug happens when code uses memory after it has been freed.

Broken example:

```zig
const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const buffer = try allocator.alloc(u8, 10);

    allocator.free(buffer);

    buffer[0] = 42;
}
```

This is invalid.

The memory lifetime ended here:

```zig
allocator.free(buffer);
```

Everything after that point is unsafe.

The slice still exists as a value, but the memory behind it no longer belongs to the program.

This is an important distinction:

```text
A pointer or slice value existing does not guarantee the memory is valid.
```

#### Lifetime of Arena Allocations

Arena allocators create grouped lifetimes.

Example:

```zig
const std = @import("std");

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(
        std.heap.page_allocator,
    );
    defer arena.deinit();

    const allocator = arena.allocator();

    const a = try allocator.dupe(u8, "red");
    const b = try allocator.dupe(u8, "blue");

    std.debug.print("{s} {s}\n", .{ a, b });
}
```

The strings `a` and `b` stay valid until:

```zig
arena.deinit();
```

After that, all arena allocations become invalid together.

This gives one large shared lifetime:

```text
everything allocated from this arena
dies when the arena dies
```

That is why arenas work well for temporary grouped data.

#### Lifetime of Fixed Buffer Allocations

A fixed buffer allocator depends on the backing buffer lifetime.

```zig
var memory: [1024]u8 = undefined;

var fba = std.heap.FixedBufferAllocator.init(&memory);
const allocator = fba.allocator();
```

All allocations come from `memory`.

That means:

```text
allocated values stay valid only while memory stays valid
```

If `memory` is a local variable, the allocations die when the function returns.

Broken example:

```zig
const std = @import("std");

fn makeName() ![]u8 {
    var memory: [64]u8 = undefined;

    var fba = std.heap.FixedBufferAllocator.init(&memory);
    const allocator = fba.allocator();

    return try allocator.dupe(u8, "Ada");
}
```

The returned slice points into `memory`, but `memory` dies when the function returns.

That creates a dangling slice again.

#### Containers Also Have Lifetimes

Containers often own heap memory internally.

Example:

```zig
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
```

The list owns its internal buffer.

The buffer lifetime ends when:

```zig
list.deinit();
```

After deinitialization, using `list.items` is invalid.

Broken example:

```zig
const items = list.items;

list.deinit();

std.debug.print("{d}\n", .{items[0]});
```

`items` points into memory owned by the list. After `deinit`, that memory is gone.

The slice survived. The memory did not.

#### Borrowed Memory vs Owned Memory

Some slices are borrowed.

Borrowed memory means:

```text
you can use it temporarily
you do not own it
you must not free it
```

Example:

```zig
fn printName(name: []const u8) void {
    std.debug.print("{s}\n", .{name});
}
```

This function borrows `name`.

It does not allocate it. It does not free it. The caller keeps ownership.

Other functions return owned memory:

```zig
fn makeName(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.dupe(u8, "Ada");
}
```

The caller now owns the returned slice and must free it.

Understanding this difference is critical.

#### A Useful Mental Question

Whenever you see a slice or pointer, ask:

```text
Who owns the memory?
How long does it stay valid?
Who frees it?
```

These questions prevent many lifetime bugs.

#### Lifetime Transfer

Ownership can move from one place to another.

Example:

```zig
fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.alloc(u8, 1024);
}
```

Inside the function:

```text
the function temporarily owns the allocation
```

After returning:

```text
the caller owns the allocation
```

This is ownership transfer.

The cleanup responsibility moves with it.

#### `defer` Helps Express Lifetime

`defer` matches cleanup to scope lifetime.

```zig
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
```

This says:

```text
buffer is valid for the rest of this scope
free it when the scope ends
```

This keeps lifetime logic close to allocation logic.

That reduces mistakes.

#### Nested Lifetimes

Some values depend on other values.

Example:

```zig
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();

const allocator = gpa.allocator();

var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
```

The list depends on the allocator.

So the list must die before the allocator dies.

Because `defer` runs in reverse order:

```text
list.deinit() runs first
gpa.deinit() runs second
```

That is correct.

Think of this as lifetime nesting:

```text
allocator lifetime contains list lifetime
```

The dependency must outlive the thing that depends on it.

#### A Common Beginner Mistake

Broken code:

```zig
fn buildList(
    allocator: std.mem.Allocator,
) ![][]const u8 {
    var list = std.ArrayList([]const u8).init(allocator);
    defer list.deinit();

    try list.append("red");
    try list.append("blue");

    return list.items;
}
```

This returns `list.items`, but `list.deinit()` frees the internal storage before the caller receives it.

Correct approach:

```text
either transfer ownership safely
or keep the container alive
```

A correct ownership-transfer design often uses:

```zig
return try list.toOwnedSlice();
```

because ownership moves from the container to the caller.

#### Lifetimes Are a Design Problem

Lifetime management is not merely a cleanup detail.

It affects API design.

A good Zig API makes ownership clear:

```text
Does the function borrow memory?
Does it allocate new memory?
Who owns the result?
Who frees it?
How long is the data valid?
```

When those answers are visible, the code becomes easier to reason about.

#### The Core Idea

Lifetime management means ensuring memory stays valid exactly as long as it is needed, and no longer.

Most memory bugs come from one of these mistakes:

```text
using memory after it died
freeing memory too early
never freeing memory
returning references to dead memory
forgetting who owns memory
```

The rule is:

```text
Every pointer or slice must refer to memory whose lifetime still exists.
```

