Page cover

RAII

Introduction

While developing offensive security tools (OST) and Windows internals utilities, I quickly learned that resource management can make or break your code. Handle leaks, dangling pointers, and forgotten cleanup calls aren't just bugs, they're security vulnerabilities and operational failures waiting to happen. After debugging one too many tools that crashed due to resource exhaustion or left processes in unstable states, I discovered the elegance and power of Resource Acquisition Is Initialization (RAII).

RAII transformed how I write security tooling. What used to require careful tracking of every CloseHandle(), VirtualFree(), and cleanup call across multiple error paths became automatic and bulletproof. Whether I'm working with process handles, heap allocations, token impersonation, or critical sections, RAII ensures my resources are properly managed, even when exceptions fly or early returns execute.

This blog post is my attempt to share what I've learned. If you're building Windows security tools, dealing with the Win32 API, or just tired of hunting down resource leaks, I hope this guide helps you as much as RAII has helped me.

Resource Acquisition Is Initialization (RAII) is a fundamental C++ programming idiom that ties resource management to object lifetime. The core principle:

  • Acquire resources in the constructor

  • Release resources in the destructor

  • Resources are automatically managed when objects go out of scope

The Traditional Dynamic Memory Pattern

A common programming pattern follows these steps:

  1. Allocate memory dynamically

  2. Store the memory address in a pointer

  3. Use that pointer to work with the memory

  4. Deallocate the memory when finished

The Risk: If an exception occurs after successful memory allocation but before the delete or delete[] statement executes, you get a memory leak.

#include <iostream>
#include <stdexcept>

bool someCondition(int scenario)
{
    switch (scenario)
    {
    case 1: return false;
    case 2: return true;
    default: return false;
    }
}

void ProblematicFuntion(int scenario)
{
    int* data = new int(42);

    std::cout << "Value stored: " << *data << '\n';
    std::cout << "Address: " << data << '\n';

    if (someCondition(scenario))
    {
        std::cout << "Exception THROWN!\n";
        throw std::runtime_error("Something went wrong!");
    }

    std::cout << "Function ends normally\n";
    delete data;
}
int main()
{
    try
    {
        ProblematicFuntion(2);
    }
    catch (const std::runtime_error& e)
    {
        std::cout << "Exception caught in main: " << e.what() << '\n';
    }

    return 0;
}

The issue: If an exception is thrown between new and delete, the memory is never freed.

The Solution: The C++ Core Guidelines recommend managing resources like dynamic memory using RAII (Resource Acquisition Is Initialization).

The RAII Principle

The concept is straightforward: For any resource that must be returned to the system when the program finishes using it, the program should:

  • Use that object as necessary in your program, then

  • When the function call terminates, the object goes out of scope

  • The object's destructor automatically releases the resource

The Three Pillars of RAII

Most bugs come from this false assumption: "If I have a pointer, I own the memory."

❌ That is not true.

A raw pointer does exactly one thing: It stores an address. That’s it.

  • No ownership.

  • No lifetime.

  • No responsibility.

What a raw pointer actually represents

This means only: "p can point to an int somewhere."

It does not answer:

  • Who allocated it?

  • Who deletes it?

  • How long it lives?

  • Is it valid right now?

A raw pointer is non-owning by default.

Ownership is a contract, not a type (unless you use RAII)

Ownership means responsibility.

If you own a resource, you must:

  1. Release it exactly once

  2. Release it on every exit path

  3. Not release it after it’s released

  4. Not let someone else release it

Raw pointers cannot enforce this.

Concrete RAII Example - Custom Smart Pointer

Walkthrough of the code (line by line)

This is the raw resource, RAII does not remove raw resources - it contains them.

This is the acquisition step.

Important observations:

  • Ownership is transferred here

  • After construction, this object is responsible for deletion

  • Initialization in RAII refers to object initialization, not variable assignment

This line is the moment of responsibility transfer.

This is the release step.

Crucial RAII rule:

The destructor must always leave the program in a valid state.

That means:

  • It must not leak

  • It must not throw

  • It must tolerate ptr == nullptr

They do two different but related things.

  1. Deleting the copy constructor:

What this syntactically means

  • This is the copy constructor

  • = delete tells the compiler: This function is forbidden to exist

If anyone tries to copy your object → compile-time error.

Imagine this code without deleting the copy constructor:

After the copy:

  • a.ptr → points to memory

  • b.ptr → points to the same memory

So now you have 2 owners of the same resource

What happens next?

When the scope ends:

  1. b is destroyed → delete ptr

  2. a is destroyed → delete ptr again ❌

That is:

  • Double delete

  • Undefined behavior

  • Heap corruption

  1. Deleting copy assignment

This blocks a different kind of copy.

Without this, this would compile:

What would happen?

Let’s simulate it:

  1. b already owns memory (new int(2))

  2. Assignment overwrites b.ptr with a.ptr

  3. Original memory owned by b is leaked

  4. Now both a and b point to same memory

  5. Double delete later 💥

So copy assignment causes:

  • Memory leak

  • Double delete

  • Ownership confusion

This is ownership enforcement.

Why this matters:

  • Two objects owning the same pointer = double delete

  • RAII requires clear ownership rules

By deleting copy:

  • Ownership is exclusive

  • Cleanup happens exactly once

This is fundamental RAII design, not optional.

These give the illusion: "This behaves like a pointer”

But importantly:

  • The pointer is guarded

  • You cannot forget to delete it

Code using our smart pointer

That single line does three things:

  1. Allocates memory (new int(42))

  2. Transfers ownership to SimpleSmartPtr

  3. Guarantees cleanup via destructor

There is no delete anywhere in the function ProblematicFuntion().

What happens when the exception is thrown (step-by-step)

Let’s simulate scenario == 2.

  1. data is constructed on the stack → owns the heap memory

  2. throw std::runtime_error(...) executes

  3. Stack unwinding begins → ProblematicFuntion is exited

  4. Local objects are destroyed → ~SimpleSmartPtr() is called

  5. Destructor executes:

  6. Memory is freed before control reaches main

This happens even though you never wrote cleanup code in the function.

That’s RAII.

circle-info

SimpleSmartPtr is:

  • Single-owner

  • Non-copyable

  • Exception-safe

But it is not:

  • Array-safe (delete[])

  • Move-enabled

  • Thread-safe

lvalue vs rvalue

  • lvalue → has an identity (you can point to it, it lives somewhere)

  • rvalue → is a temporary value (no stable identity)

The names come from left-hand side and right-hand side of assignments, but modern C++ meaning is a bit deeper.

lvalue

An lvalue:

  • Has a memory address

  • Can usually appear on the left side of =

  • Represents an object that persists beyond a single expression

  • xlvalue

  • 10rvalue

You can take the address of an lvalue:

More lvalue examples:

rvalue

An rvalue:

  • Is a temporary

  • Usually cannot be assigned to

  • Often destroyed at the end of the expression

  • x + 3rvalue

You cannot take its address:

More rvalue examples:

This fails:

lvalue reference (T&)

A reference that can bind only to lvalues:

Use this when:

  • You want to modify an existing object

  • You want to avoid copying

rvalue reference

Introduced in C++11.

An rvalue reference:

  • Binds to temporaries

  • Enables move semantics

But this is tricky:

Why this exists

To steal resources from temporaries instead of copying:

circle-info

A named rvalue reference is an lvalue

Because r has a name → it has identity → lvalue

Functions that return by reference return an lvalue, and functions that return by value return an rvalue.

Key idea (one line)

👉 The return type decides the value category of the function call expression.

Case 1: Return by value → rvalue (prvalue)

What is f()?

  • f() produces a temporary value

  • No stable identity

  • You cannot assign to it

So:

f() is an rvalue

Why? Because returning by value means “give me a copy / temporary”, not a specific object.

Case 2: Return by lvalue reference → lvalue

What is g()?

  • g() refers to a real, named object

  • Has identity

  • Can be assigned to

So:g() is an lvalue

Why? Because returning T& means “this function call is an object”, not a temporary.

Case 3: Return by rvalue reference → xvalue

Now:

  • Has identity

  • Marked as expiring

  • Move allowed

So: h() is an xvalue (a kind of rvalue)

The rule

Function return type

f() is

T

rvalue

T&

lvalue

T&&

xvalue

Why the language does this

Because the compiler must answer questions like:

  • Can I assign to f()?

  • Can I take &f()?

  • Should I call copy or move?

  • Which overload should I pick?

And the return type answers all of those.

Concrete mental model

Returning by value

  • Here’s a value, do whatever you want with it.

Returning by reference

  • Here’s the actual object, go talk to it.

Returning by rvalue reference

  • Here’s the object, but it’s about to be looted.

RAII wrapper for VirtualAlloc/VirtualFree

What Problem Does This Solve?

The Old C-Style Way

Problems:

  • ❌ Easy to forget VirtualFree()

  • ❌ Multiple return paths = multiple places to free

  • ❌ If exception thrown, memory leaked

  • ❌ No way to transfer ownership safely

  • ❌ Boilerplate error checking

The Modern C++ Way

Benefits:

  • ✅ Impossible to forget cleanup

  • ✅ Exception-safe automatically

  • ✅ Ownership is clear

  • ✅ Transfer ownership with move semantics

  • ✅ Zero runtime overhead

Why RAII Is Powerful

circle-info

Key insight: C++ guarantees destructors are called during stack unwinding!

Constructor Deep Dive

Why use initializer list instead of assignment?

For primitives like SIZE_T, same performance. But for objects:

circle-info

Always initialize in declaration order!

Destructor Deep Dive

Why Check if (m_ptr)?

Scenario: Moved-from object

Without the check:

Destructor Rules

Why? If an exception is already being handled and destructor throws → std::terminate() called!

The Rule of Five

Modern C++ has the Rule of Five:

If you define any of these five, you should consider all five:

  1. Destructor

  2. Copy Constructor

  3. Copy Assignment Operator

  4. Move Constructor

  5. Move Assignment Operator

Why All Five?

Solution: Delete Copy, Implement Move

Move Semantics Explained

What is std::move?

std::move doesn't actually move anything! It just casts to rvalue reference:

Move Constructor Breakdown

Step-by-Step Execution

Visual representation:

Move Assignment Breakdown

Why Self-Assignment Check?

Without it:

With check:

Why Return *this?

Enables chaining:

Template Member Functions

Breaking Down Each Part

1. Template Syntax

How it works:

Each instantiation creates a new function!

2. Return Type T*

Examples:

3. const Member Function

What does const mean here?

Why make it const?

4. noexcept Specifier

Meaning: This function guarantees it will never throw an exception.

Benefits:

Contrast with throwing version:

5. static_cast<T*>

What is static_cast?

C++ has several types of casts:

Why static_cast for void*?

Why not reinterpret_cast?

Full C++ implementation

Example 1: Basic Usage

Example 2: Move Semantics

Modern C++ Features Summary

This class demonstrates:

  1. RAII - Resource management tied to object lifetime

  2. Move Semantics - Efficient transfer of ownership

  3. Rule of Five - Proper special member function handling

  4. Deleted Functions - Prevent unwanted operations

  5. noexcept - Exception guarantees for optimization

  6. Template Member Functions - Generic programming

  7. const Correctness - Immutability guarantees

  8. In-Class Initializers - Default member values

  9. Uniform Initialization - Consistent syntax

  10. std::format (C++20) - Type-safe formatting

  11. Explicit Constructors - Prevent implicit conversions

  12. Scope Resolution - Explicit namespace lookup

Conclusion

This VirtualMemory class is a perfect example of modern C++ philosophy:

Zero-cost abstractions with maximum safety

You get:

  • ✅ Memory safety (no leaks, no double-free)

  • ✅ Exception safety (RAII guarantees cleanup)

  • ✅ Move efficiency (no unnecessary copies)

  • ✅ Clear ownership semantics

  • Zero runtime overhead

All while writing less code and making fewer mistakes than manual C-style management!

Last updated