We’re Officially Live β€” Lifetime Founding Membership Available for a Limited Time
Free preview

Constructors, Destructors & RAII

Tie setup to object creation and cleanup to scope exit, so you can't forget to release a lock, disable a clock, or re-enable interrupts. The defining C++ idiom for embedded.

25 min read

In this lesson:


The problem RAII solves

In C, setup and teardown are manual and easy to forget:

void process(void) {
    uart_enable_clock();          // acquire
    /* ... lots of code, early returns, error paths ... */
    if (error) return;            // BUG: forgot to disable the clock!
    uart_disable_clock();         // only runs on the happy path
}

Every early return is a chance to leak the resource. RAII ties the cleanup to the object's lifetime so the compiler runs it for you on every exit path, normal return, early return, anything.


Constructors: establish a valid object

A constructor is a special method, named after the class, that runs automatically when an object is created. Its job is to put the object into a valid state, initialize members, configure hardware, acquire resources.

class Uart {
    volatile uint32_t *base_;
    uint32_t baud_;
public:
    Uart(volatile uint32_t *base, uint32_t baud)   // constructor
        : base_(base), baud_(baud)                  // member initializer list
    {
        // configure registers using base_ and baud_
    }
};
 
Uart console(UART1_BASE, 115200);   // constructor runs here, automatically

The member initializer list

The : base_(base), baud_(baud) part is the member initializer list. It initializes members directly, before the constructor body runs. Prefer it over assigning in the body:

Uart(uint32_t baud) : baud_(baud) { }   // GOOD: baud_ initialized once
 
Uart(uint32_t baud) { baud_ = baud; }    // WORSE: baud_ default-inited, THEN assigned

For const members and reference members the initializer list is mandatory, they can't be assigned after the fact. Note: members initialize in declaration order, not the order you list them, list them in declaration order to avoid confusion.


Destructors: automatic cleanup

A destructor (~ClassName) runs automatically when an object's lifetime ends, when a local goes out of scope, or when a heap object is deleted. It's where you release whatever the constructor acquired.

class ClockGate {
    uint32_t periph_;
public:
    ClockGate(uint32_t periph) : periph_(periph) {
        RCC->ENR |= periph_;       // acquire: enable the peripheral clock
    }
    ~ClockGate() {
        RCC->ENR &= ~periph_;      // release: disable it β€” runs automatically
    }
};

RAII: Resource Acquisition Is Initialization

Put those together and you get RAII: acquire the resource in the constructor, release it in the destructor, and let scope drive the lifetime. The resource is now impossible to leak:

void process(void) {
    ClockGate gate(RCC_UART1);     // clock ON here
 
    if (error) return;             // destructor runs β†’ clock OFF
    do_work();
}                                  // destructor runs here too β†’ clock OFF

Both exit paths disable the clock, because the compiler inserts the destructor call at every point gate leaves scope. You cannot forget. This is the single biggest reason to use C++ on embedded: deterministic, automatic, leak-proof resource management with zero runtime overhead.

And note "deterministic": unlike garbage-collected languages, C++ destruction happens at a known, precise moment, the closing brace, not whenever a collector decides. That predictability is exactly what real-time firmware needs.


The canonical embedded RAII: a critical-section guard

The textbook embedded use of RAII is a scoped lock that disables interrupts on entry and restores them on exit, protecting a read-modify-write of shared state (recall the non-atomic RMW problem from the C module):

class CriticalSection {
    uint32_t primask_;
public:
    CriticalSection() {
        primask_ = __get_PRIMASK();   // save current interrupt-enable state
        __disable_irq();              // acquire: enter critical section
    }
    ~CriticalSection() {
        __set_PRIMASK(primask_);      // release: restore previous state exactly
    }
};
 
volatile uint32_t shared_counter;
 
void increment(void) {
    CriticalSection cs;        // interrupts off
    shared_counter++;          // safe read-modify-write
}                              // interrupts restored β€” on every path

This guard restores the previous state (via PRIMASK) rather than blindly re-enabling, so it nests correctly. One named object replaces error-prone paired __disable_irq()/__enable_irq() calls that you might mismatch.


Destruction order

Objects are destroyed in reverse order of construction, and this is guaranteed, it's what makes nested resources unwind correctly:

void f() {
    ClockGate gate(RCC_SPI1);   // constructed 1st
    SpiDevice dev(SPI1);        // constructed 2nd
    // ...
}   // dev destroyed 1st, then gate β€” reverse order, so the device
    // shuts down before its clock is gated. Correct by construction.

Gotchas


TL;DR

This is just the start

Sign up free to track progress on this lesson, get coding problems with the Run button, and unlock the full interview Q&A.

More in C++ for Embedded

C++ vs C for EmbeddedClasses & Encapsulation
References vs Pointers
const Correctness & constexpr
Function & Operator Overloading
Templates: The Basics
Constructors, Destructors & RAII | EmbeddedPrep.io