Skip to content
BoKSA

Constructors, RAII, and Destructors

Constructors, RAII, and Destructors

Introduction

In C, initialization is manual: motor_init(&m); before use, and cleanup is easy to forget: file_close never called on error path. C++ constructors run when an object is created; destructors run when it goes out of scope or is deleted. Together they enable RAII (Resource Acquisition Is Initialization) — one of the main reasons to use C++ on embedded projects.

If you take one idea from this section, take RAII: tie a resource (pin, mutex, file, DMA buffer) to an object's lifetime so release is automatic.


Constructors — setup on birth

A constructor has the same name as the class and no return type:

1
2
3
4
5
6
7
8
9
class UartTx {
public:
    UartTx(int baud) : baud_(baud) {
        hal_uart_init(baud_);
    }

private:
    int baud_;
};

Creating the object runs the constructor:

1
2
3
void setup() {
    UartTx console(115200);  /* hal_uart_init called here */
}

Member initializer list

The : baud_(baud) part is the initializer list — preferred for members before the body runs:

1
2
3
4
5
Motor::Motor(int port, float max_rpm)
    : port_(port), max_rpm_(max_rpm), rpm_(0.0f)
{
    gpio_configure(port_);
}

Use it for const members, references, and base classes — assignment inside { } is too late for those.


Destructors — cleanup on death

A destructor is ~ClassName():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class ScopedLock {
public:
    explicit ScopedLock(Mutex &m) : mutex_(m) { mutex_.lock(); }
    ~ScopedLock() { mutex_.unlock(); }

private:
    Mutex &mutex_;
};

void updateShared(void) {
    ScopedLock lock(shared_mutex);
    /* ... critical section ... */
}   /* destructor runs here — always unlocks */

Even if you return early or throw (where exceptions are enabled), destructor runs when lock goes out of scope.

In plain terms

RAII = "I bought a ticket when I entered; the ticket is destroyed when I leave, and the door locks itself." You cannot forget to unlock if the language guarantees the destructor runs at scope exit.


RAII on embedded (no exceptions needed)

RAII works without exceptions. Scope exit still calls destructors when:

  • Block ends with }
  • Function returns
  • Object is deleted with delete

GPIO example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class PinGuard {
public:
    explicit PinGuard(int pin) : pin_(pin) {
        gpio_set_mode(pin_, OUTPUT);
        gpio_write(pin_, HIGH);
    }
    ~PinGuard() {
        gpio_write(pin_, LOW);  /* safe default on exit */
    }
private:
    int pin_;
};

void pulse(int pin) {
    PinGuard enable(pin);
    busy_wait_ms(10);
}  /* pin driven low automatically */

Mutex in RTOS wrapper:

1
2
3
4
5
6
7
8
9
class LockGuard {
public:
    explicit LockGuard(SemaphoreHandle_t sem) : sem_(sem) {
        xSemaphoreTake(sem_, portMAX_DELAY);
    }
    ~LockGuard() { xSemaphoreGive(sem_); }
private:
    SemaphoreHandle_t sem_;
};

Same pattern as std::lock_guard in desktop C++ — you can implement a thin version without full STL.


Construction order and static objects

Within one object: base classes → members in declaration order → constructor body.

Global / static objects: constructed before main (order between translation units is a classic footgun). On embedded, many teams avoid non-trivial static constructors and use explicit init() for predictability — see Embedded C++ subset.

1
2
// Risky on some embedded setups:
static UartTx console(115200);  /* runs before main */

Prefer:

1
2
UartTx *console = nullptr;
void board_init() { console = new UartTx(115200); }  /* or static storage + placement */

Or a plain C init if your toolchain's static init is unclear.


new and delete — use carefully

Constructors pair with dynamic allocation:

1
2
3
Sensor *s = new Sensor(ADC1_CH3);
s->read();
delete s;

On MCU, prefer static or pool allocation; if you use new, handle out-of-memory (new (std::nothrow) returns nullptr). Destructor runs on delete.

Smart pointers (std::unique_ptr) wrap delete in RAII — useful on embedded Linux; on bare metal often replaced with custom pool handles.


Copy and move (awareness)

C++ can copy or move objects. Embedded firmware often deletes copy on hardware-owning classes:

1
2
3
4
5
6
class Uart {
public:
    Uart(const Uart &) = delete;
    Uart &operator=(const Uart &) = delete;
    // ...
};

Prevents two objects from thinking they own the same peripheral. Details in advanced courses; rule of thumb: disable copy for drivers unless you mean to duplicate.


Relevant topics


Starting points

  1. Write ScopedLock around an existing mutex in your RTOS project.
  2. Trace one constructor in Arduino Serial.begin path conceptually — what gets configured?
  3. List three resources in your code that need cleanup — map each to a destructor plan.
  4. grep static globals in a C++ firmware tree — flag any with non-trivial constructors.

Focus points

  • Destructor must not throw (if exceptions exist) and must be fast in ISR context — do not destroy complex objects in ISR.
  • RAII replaces goto cleanup chains in C — still document ownership.
  • Static initialization order — avoid surprises; prefer explicit init on MCU.
  • Delete copy on peripheral-owning classes unless shallow copy is safe.

Key points

  • Constructors initialize objects; destructors clean up when lifetime ends.
  • RAII binds resources to scope — locks, pins, files release automatically.
  • Initializer lists are the idiomatic way to set members.
  • On embedded, prefer scope-based cleanup over manual free/close/unlock scattered in code.