In this lesson:
- constructors: run automatically at creation to establish a valid object
- the member initializer list (and why it beats assignment in the body)
- destructors: run automatically at scope exit to release resources
- RAII, the idiom that makes "you can't forget to clean up" a compile-time guarantee
- the canonical embedded RAII: a critical-section / interrupt guard
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, automaticallyThe 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 assignedFor 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 OFFBoth 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 pathThis 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
- An RAII guard must be a named variable.
CriticalSection();(no name) creates a temporary that's destroyed at the end of the full expression, interrupts turn off and back on immediately, protecting nothing. Always name it:CriticalSection cs;. - Never let a destructor throw. If a destructor throws (and exceptions are enabled), and it's running during stack unwinding from another exception, the program calls
std::terminate. On embedded you typically disable exceptions anyway, but the rule stands: destructors must not fail. - Member init order = declaration order, not list order. If
b_depends ona_, declarea_first; the compiler initializes by declaration order regardless of how you order the initializer list (and warns with-Wreorder). - Rule of zero / three / five. If your class manages a resource and you write a destructor, you usually also need to handle copying (copy constructor + assignment) or disable it, otherwise a copy double-frees. Best: design so members clean up themselves and you write no special functions (the "rule of zero").
TL;DR
- A constructor runs automatically at creation to establish a valid object; use the member initializer list (
: a_(x), b_(y)), mandatory forconst/reference members. - A destructor (
~Class) runs automatically at scope exit ordelete, releasing what the constructor acquired. - RAII ties resource lifetime to object lifetime, so cleanup runs on every exit path, you can't leak a clock, lock, or buffer. It's deterministic (exact timing), unlike garbage collection.
- The defining embedded idiom is a scoped critical-section guard: disable interrupts in the constructor, restore the saved state in the destructor.
- Objects destruct in reverse construction order; name your RAII guards (a temporary dies immediately), and follow the rule of zero to avoid copy/double-free bugs.