Volume 03 Beginner 4 sub-modules ~20 min read

RAII: Cleanup You Cannot Forget

Most firmware engineers have met a function with an early return that forgot to give something back: a bus lock, a chip select, interrupts left off. RAII puts the giving back in a destructor, so the compiler runs it on every way out. This volume builds guards for interrupts, chip selects and clocks, measures what they cost, and ends with the rule that keeps them safe.

You will learn
  • Why cleanup is forgotten on early returns in C, and how a destructor ends that for good
  • How to write an interrupt lock that saves and restores the old state, and nests safely
  • Guard objects for chip selects and peripheral clocks
  • Two ways a guard can silently do nothing, and how to spot them
  • What a copy does to a class that gives something back, and the rule of zero, three and five
You need
  • Volume 02 of this course: constructors, destructors and static members.
  • Critical sections, from Embedded C from Zero's interrupts volume.

3.1 Resources that clean up after themselves

Take a resource in a constructor and give it back in the destructor. Then it is given back on every way out of the scope, because the compiler puts the destructor call at each one. That is RAII.

Firmware is full of things that must be given back. A lock on a shared bus, interrupts that were switched off, a chip-select line held low and a peripheral clock switched on are all examples. In C, every way out of a function has to remember to give them back. A function with three return statements has three chances to forget.


// raii_basics.cpp - a cleanup that every path out of a function runs
#include <cstdio>

// A shared bus, and a count of who holds it: 0 means free.
static int bus_holders = 0;
static void bus_lock() { bus_holders++; }
static void bus_unlock() { bus_holders--; }

// The C way: every way out of the function must remember to unlock.
static int read_sensor_c(int channel) {
    bus_lock();
    if (channel > 3) {
        return -1;                  // this path forgets bus_unlock()
    }
    int value = channel * 100;
    bus_unlock();
    return value;
}

// The C++ way: the unlock lives in a destructor, and every way out runs it.
class BusLock {
public:
    BusLock() { bus_lock(); }
    ~BusLock() { bus_unlock(); }
};

static int read_sensor(int channel) {
    BusLock lock;
    if (channel > 3) {
        return -1;                  // the destructor runs here too
    }
    return channel * 100;
}

// The call has finished before report() runs, so bus_holders is read afterwards.
static void report(const char *who, int channel, int result) {
    std::printf("%-4s channel %d: returned %4d, bus holders afterwards %d\n", who, channel, result, bus_holders);
}

int main() {
    report("C", 2, read_sensor_c(2));
    report("C", 9, read_sensor_c(9));
    bus_holders = 0;
    report("C++", 2, read_sensor(2));
    report("C++", 9, read_sensor(9));
    return 0;
}

C    channel 2: returned  200, bus holders afterwards 0
C    channel 9: returned   -1, bus holders afterwards 1
C++  channel 2: returned  200, bus holders afterwards 0
C++  channel 9: returned   -1, bus holders afterwards 0

The C version works on the good channel and leaks on the bad one. After channel 9 the bus is still held, and the next piece of code that wants it will wait for ever. The C++ version gives the bus back both times.

The class BusLock is a guard object. Its constructor takes the lock and its destructor gives it back. The function read_sensor never mentions unlocking at all. It makes a BusLock, and the compiler places the destructor call at both return statements, as Volume 02 showed with Trace. The name RAII stands for "resource acquisition is initialisation": getting the resource is part of making the object.

Think of it like this

Think of a door with a spring that closes it behind you. You cannot forget to shut it, however you leave the room - walking out calmly, or running out because something went wrong. A guard object is that spring.

What a guard costs

The size harness compiled a correct C version, with an unlock written on each path, and the RAII version of the same function. The RAII version came out smaller: 39 bytes against 44. The compiler saw that both ways out end with the same unlock, so it wrote one call and jumped to it from both paths. With exceptions switched off, nothing else is added.

The two functions, and their machine code

int read_locked_c(int channel) {
    bus_lock();
    if (channel > 3) {
        bus_unlock();
        return -1;
    }
    int value = read_register(channel);
    bus_unlock();
    return value;
}

class BusLock {
public:
    BusLock() { bus_lock(); }
    ~BusLock() { bus_unlock(); }
};

int read_locked_raii(int channel) {
    BusLock lock;
    if (channel > 3) {
        return -1;
    }
    return read_register(channel);
}

read_locked_c(int):
    push   %rbx
    mov    %edi,%ebx
    call   bus_lock()
    cmp    $0x3,%ebx
    jg     <+0x20>
    mov    %ebx,%edi
    call   read_register(int)
    mov    %eax,%ebx
    call   bus_unlock()
    mov    %ebx,%eax
    pop    %rbx
    ret
    nop
    call   bus_unlock()
    mov    $0xffffffff,%ebx
    jmp    <+0x1b>
read_locked_raii(int):
    push   %rbx
    mov    %edi,%ebx
    call   bus_lock()
    cmp    $0x3,%ebx
    jg     <+0x20>
    mov    %ebx,%edi
    call   read_register(int)
    mov    %eax,%ebx
    call   bus_unlock()
    mov    %ebx,%eax
    pop    %rbx
    ret
    nop
    mov    $0xffffffff,%ebx
    jmp    <+0x16>
-> not the same: 44 bytes and 39 bytes

Follow the bad channel in each. In read_locked_c, jg <+0x20> jumps to its own call bus_unlock(), then jumps back to the shared return. In read_locked_raii, the jump lands on the one call bus_unlock() that the good path uses too.

Common mistake

Giving the guard no name. The line BusLock(); makes a temporary object, which is destroyed at the end of that very statement. That is before the work it was meant to protect, and it compiles without a warning:


// temporary.cpp - a guard with no name lives for one line only
#include <cstdio>

class BusLock {
public:
    BusLock() { std::printf("  lock taken\n"); }
    ~BusLock() { std::printf("  lock released\n"); }
};

static void with_a_name() {
    BusLock lock;
    std::printf("  using the bus\n");
}

static void without_a_name() {
    BusLock();                      // made, and destroyed again, on this one line
    std::printf("  using the bus\n");
}

int main() {
    std::printf("with a name:\n");
    with_a_name();
    std::printf("without a name:\n");
    without_a_name();
    return 0;
}

with a name:
  lock taken
  using the bus
  lock released
without a name:
  lock taken
  lock released
  using the bus

The opposite slip, empty brackets after the name, is caught. The line BusLock lock(); is read as the declaration of a function called lock. This is the most vexing parse, and g++ warns about it:


// vexing.cpp - empty brackets after the name
class BusLock {
public:
    BusLock();
    ~BusLock();
};

void use_the_bus() {
    BusLock lock();
}

vexing.cpp: In function 'void use_the_bus()':
vexing.cpp:9:17: error: empty parentheses were disambiguated as a function declaration [-Werror=vexing-parse]
vexing.cpp:9:17: note: remove parentheses to default-initialize a variable
Quick check

In raii_basics.cpp, why is the bus free after read_sensor(9) returns early?

Show the answer

Answer: C. The guard object lock is destroyed on every way out of the function, and its destructor calls bus_unlock(). The C version has no destructor, so its early return left the bus held: the output shows 1 holder afterwards.

3.2 Scoped interrupt locks

An interrupt lock is a guard object for a critical section. When it is made, it saves the interrupt state and switches interrupts off. When it is destroyed, it puts the saved state back, so locks can nest and no path can leave interrupts off.

Embedded C from Zero, Volume 11 wrote a critical section by hand: save the state and disable interrupts, do the work, restore the state. It also warned against ending with a blind "enable interrupts", which switches them on underneath a caller who had them off. Here is that rule, built into a class, with the blind version beside it for comparison.


// irq_lock.cpp - a critical section that cannot be left open, and nests safely
#include <cstdint>
#include <cstdio>

// The chip's interrupt mask, faked for the PC: 1 means interrupts are off.
// On an Arm Cortex-M chip these four are CMSIS's __get_PRIMASK(), __set_PRIMASK(),
// __disable_irq() and __enable_irq().
static uint32_t primask = 0;
static uint32_t get_primask() { return primask; }
static void set_primask(uint32_t value) { primask = value; }
static void disable_irq() { primask = 1; }
static void enable_irq() { primask = 0; }

// Save the old state, switch interrupts off, and put the old state back at the end.
class InterruptLock {
public:
    InterruptLock() : saved_(get_primask()) { disable_irq(); }
    ~InterruptLock() { set_primask(saved_); }
    InterruptLock(const InterruptLock &) = delete;              // no copies: see 3.4
    InterruptLock &operator=(const InterruptLock &) = delete;

private:
    uint32_t saved_;
};

// The mistake: switch interrupts back on at the end, whatever they were before.
class BlindLock {
public:
    BlindLock() { disable_irq(); }
    ~BlindLock() { enable_irq(); }
};

static uint32_t total = 0;

static void add_reading(uint32_t reading) {
    InterruptLock lock;
    total = total + reading;
}

static void add_reading_blind(uint32_t reading) {
    BlindLock lock;
    total = total + reading;
}

static const char *irq() { return get_primask() ? "off" : "on"; }

int main() {
    std::printf("at the start:                   interrupts %s\n", irq());
    add_reading(10);
    std::printf("after add_reading:              interrupts %s\n", irq());
    {
        InterruptLock outer;
        std::printf("outer lock taken:               interrupts %s\n", irq());
        add_reading(20);
        std::printf("after a nested InterruptLock:   interrupts %s\n", irq());
        add_reading_blind(30);
        std::printf("after a nested BlindLock:       interrupts %s\n", irq());
    }
    std::printf("after the outer lock:           interrupts %s\n", irq());
    std::printf("total = %u\n", static_cast<unsigned>(total));
    return 0;
}

at the start:                   interrupts on
after add_reading:              interrupts on
outer lock taken:               interrupts off
after a nested InterruptLock:   interrupts off
after a nested BlindLock:       interrupts on
after the outer lock:           interrupts on
total = 60

On an Arm Cortex-M chip, the interrupt switch is a one-bit register called PRIMASK. The fake functions here have the same jobs as the CMSIS ones named in the comment. The class keeps the rule from the C course in two lines:

The fifth line of the output is the bug that rule prevents. Inside the outer lock, add_reading took and released its own lock, and interrupts stayed off, because the old state was "off". Then add_reading_blind switched them on at its end - while the outer block still believed it was protected. Figure 3.1 shows the same run over time.

Interrupts during the nested locks in irq_lock.cpp InterruptLock outer, the block in main() add_reading() an InterruptLock add_reading_blind() a BlindLock on off outer lock taken: off inner lock ends: still off BlindLock ends: on, too early outer lock ends shaded: interrupts on while the outer block believes they are off
Figure 3.1 - The outer lock switches interrupts off. The nested InterruptLock puts back the state it found, which was off, so the outer block stays protected. The BlindLock switches interrupts on when it ends, and for the rest of the outer block they are on while that code believes they are off.

The two lines ending in = delete forbid copying an InterruptLock. A copy would carry its own saved_, and put the state back a second time. The last sub-module explains the rule behind this; for now, notice that the compiler enforces it:


// irq_copy.cpp - trying to copy an InterruptLock
#include <cstdint>

class InterruptLock {
public:
    InterruptLock();
    ~InterruptLock();
    InterruptLock(const InterruptLock &) = delete;
    InterruptLock &operator=(const InterruptLock &) = delete;

private:
    uint32_t saved_;
};

void update() {
    InterruptLock lock;
    InterruptLock second = lock;
}

irq_copy.cpp: In function 'void update()':
irq_copy.cpp:17:28: error: use of deleted function 'InterruptLock::InterruptLock(const InterruptLock&)'
irq_copy.cpp:8:5: note: declared here
Remember

Everything inside an interrupt lock adds to the time other interrupts must wait. Keep the lock in the smallest block that needs it: a pair of braces around three lines is often enough.

Quick check

Why does InterruptLock put back the saved state instead of simply switching interrupts on?

Show the answer

Answer: A. Locks nest. In the output, the nested InterruptLock left interrupts off, as the outer block needed. The BlindLock switched them on inside the outer block, which is the bug the saved state prevents.

3.3 Scoped peripheral enables

The same pattern fits any "switch on, use, switch off" job. A chip select can be held low for one transfer, and a peripheral's clock kept on for exactly as long as someone needs it.

A chip select that is always released

An SPI device listens only while its chip select is held low. Forget to raise it after a transfer - on an error path, say - and the device keeps listening, and the next transfer to a different device confuses both.


// chip_select.cpp - an SPI chip select that is released on every path out of a transfer
#include <cstdint>
#include <cstdio>

// A cut-down output pin, standing in for the OutputPin class from Volume 02.
class OutputPin {
public:
    void set() { high_ = true; }
    void clear() { high_ = false; }
    bool is_set() const { return high_; }

private:
    bool high_ = true;
};

// Chip select is active low: pulled low for the whole transfer, high again after it.
class ChipSelect {
public:
    explicit ChipSelect(OutputPin &cs) : cs_(cs) { cs_.clear(); }
    ~ChipSelect() { cs_.set(); }
    ChipSelect(const ChipSelect &) = delete;
    ChipSelect &operator=(const ChipSelect &) = delete;

private:
    OutputPin &cs_;
};

static OutputPin sensor_cs;
static bool sensor_present = true;

// A fake SPI exchange: a missing device answers 0xFF, as a floating data line would.
static uint8_t spi_exchange(uint8_t out) {
    if (!sensor_present) {
        return 0xFF;
    }
    std::printf("  exchanged 0x%02X while CS is %s\n", static_cast<unsigned>(out),
                sensor_cs.is_set() ? "high" : "low");
    return static_cast<uint8_t>(out + 1);
}

static int read_register(uint8_t reg) {
    ChipSelect select(sensor_cs);
    if (spi_exchange(reg) == 0xFF) {
        return -1;                      // no device: leave early, and CS still goes high
    }
    return spi_exchange(0x00);
}

int main() {
    int value = read_register(0x0F);
    std::printf("device present: read %d, CS is now %s\n", value, sensor_cs.is_set() ? "high" : "low");
    sensor_present = false;
    value = read_register(0x0F);
    std::printf("device missing: read %d, CS is now %s\n", value, sensor_cs.is_set() ? "high" : "low");
    return 0;
}

  exchanged 0x0F while CS is low
  exchanged 0x00 while CS is low
device present: read 1, CS is now high
device missing: read -1, CS is now high

Both bytes of the first transfer went out while CS was low, and CS was high again afterwards. When the device was missing, read_register gave up after the first exchange and returned early, and CS still went high. The guard holds a reference to the pin, a use of references from Volume 01 that fits well: a chip select always belongs to one pin, and cannot be null.

A clock that stays on while anyone needs it

Many chips save power by switching off the clock to each peripheral that is not in use. Switching it off too early is a bug, and so is leaving it on for ever. When several parts of the program share one peripheral, the answer is a guard with a reference count, kept in a static member as in Volume 02:


// clock_enable.cpp - a peripheral clock that stays on while anyone is using it
#include <cstdint>
#include <cstdio>

static uint32_t fake_clock_enable_reg = 0;      // one bit per peripheral clock, as on most chips
constexpr uint32_t ADC_CLOCK = 1u << 8;          // the bit for the ADC, in this made-up chip

class AdcClock {
public:
    AdcClock() {
        if (users_ == 0) {
            fake_clock_enable_reg = fake_clock_enable_reg | ADC_CLOCK;     // first user: clock on
        }
        users_++;
    }
    ~AdcClock() {
        users_--;
        if (users_ == 0) {
            fake_clock_enable_reg = fake_clock_enable_reg & ~ADC_CLOCK;    // last user gone: clock off
        }
    }
    AdcClock(const AdcClock &) = delete;
    AdcClock &operator=(const AdcClock &) = delete;

private:
    static inline uint32_t users_ = 0;
};

static const char *adc_clock() { return (fake_clock_enable_reg & ADC_CLOCK) ? "on" : "off"; }

static void check_battery() {
    AdcClock clock;
    std::printf("  check_battery: ADC clock %s\n", adc_clock());
}

static void log_temperature() {
    AdcClock clock;
    std::printf("  log_temperature: ADC clock %s\n", adc_clock());
    check_battery();                    // a second user, nested inside the first
    std::printf("  back in log_temperature: ADC clock %s\n", adc_clock());
}

int main() {
    std::printf("at the start: ADC clock %s\n", adc_clock());
    log_temperature();
    std::printf("afterwards: ADC clock %s\n", adc_clock());
    return 0;
}

at the start: ADC clock off
  log_temperature: ADC clock on
  check_battery: ADC clock on
  back in log_temperature: ADC clock on
afterwards: ADC clock off

The third line is the one a simpler guard would get wrong. When check_battery finished, its guard was destroyed, but the clock stayed on, because log_temperature still had one. Only when the last user's guard went did the clock switch off.

Common mistake

Making the guard at the top of a long function, when only a few lines need it. The chip select, the lock or the clock is then held for the whole function. Put the guard in the smallest block that needs it, and add a pair of braces to make that block if necessary.

Quick check

In clock_enable.cpp, why is the ADC clock still on after check_battery returns?

Show the answer

Answer: B. Each AdcClock adds one to users_ and takes one away when destroyed. After check_battery's guard is gone the count is back to one - log_temperature's guard - so the destructor leaves the clock on. The count reaches zero only when that last guard is destroyed.

3.4 Rule of zero, three and five, simply

A class that gives something back in its destructor must decide what copying it means. In firmware the usual answer is the simplest one: copying is not allowed.

When you do not write a copy constructor, the compiler writes one that copies each member. For most classes that is exactly right. For a class that owns something, it is a bug waiting to happen:


// double_release.cpp - a class that releases something in its destructor, copied by accident
#include <cstdio>

// A tiny pool of four DMA channels, which reports a channel released twice.
static bool channel_in_use[4] = {false, false, false, false};

static int dma_claim() {
    for (int i = 0; i < 4; i++) {
        if (!channel_in_use[i]) {
            channel_in_use[i] = true;
            return i;
        }
    }
    return -1;
}

static void dma_release(int ch) {
    if (!channel_in_use[ch]) {
        std::printf("ERROR: channel %d released twice\n", ch);
        return;
    }
    channel_in_use[ch] = false;
    std::printf("channel %d released\n", ch);
}

class DmaChannel {
public:
    DmaChannel() : ch_(dma_claim()) { std::printf("channel %d claimed\n", ch_); }
    ~DmaChannel() {
        if (ch_ >= 0) {
            dma_release(ch_);
        }
    }
    int number() const { return ch_; }

private:
    int ch_;
};

static void log_channel(DmaChannel ch) {        // by value: the caller's object is copied
    std::printf("logging channel %d\n", ch.number());
}

int main() {
    DmaChannel tx;
    log_channel(tx);
    std::printf("back in main\n");
    return 0;
}

channel 0 claimed
logging channel 0
channel 0 released
back in main
ERROR: channel 0 released twice

Passing tx by value made a copy. The copy held the same channel number, and when log_channel returned, the copy's destructor released channel 0 - while tx still believed it owned it. When main ended, tx released it again. This is the DMA version of a double free, and in a real driver the second release could take a channel away from somebody else.

The fix is to say what copying means. For a class like this, the right meaning is "not allowed", written with = delete. A deleted function cannot be called, so the mistake now stops the build:


// no_copy.cpp - the same mistake, once copying has been deleted
class DmaChannel {
public:
    DmaChannel();
    ~DmaChannel();
    DmaChannel(const DmaChannel &) = delete;
    DmaChannel &operator=(const DmaChannel &) = delete;
    int number() const { return ch_; }

private:
    int ch_;
};

void log_channel(DmaChannel ch);

void send() {
    DmaChannel tx;
    log_channel(tx);
}

no_copy.cpp: In function 'void send()':
no_copy.cpp:18:16: error: use of deleted function 'DmaChannel::DmaChannel(const DmaChannel&)'
no_copy.cpp:14:29: note: initializing argument 1 of 'void log_channel(DmaChannel)'

The second line deleted is the copy assignment, operator=, which copies one existing object over another. Deleting both closes both doors. And log_channel only needs to look at the channel, so it should take a const reference, as Volume 01 advised. Then nothing is copied at all:


// by_reference.cpp - copying deleted, and the function takes a reference instead
#include <cstdio>

static bool channel_in_use[4] = {false, false, false, false};

static int dma_claim() {
    for (int i = 0; i < 4; i++) {
        if (!channel_in_use[i]) {
            channel_in_use[i] = true;
            return i;
        }
    }
    return -1;
}

static void dma_release(int ch) {
    channel_in_use[ch] = false;
    std::printf("channel %d released\n", ch);
}

class DmaChannel {
public:
    DmaChannel() : ch_(dma_claim()) { std::printf("channel %d claimed\n", ch_); }
    ~DmaChannel() {
        if (ch_ >= 0) {
            dma_release(ch_);
        }
    }
    DmaChannel(const DmaChannel &) = delete;
    DmaChannel &operator=(const DmaChannel &) = delete;
    int number() const { return ch_; }

private:
    int ch_;
};

static void log_channel(const DmaChannel &ch) {     // a reference: nothing is copied
    std::printf("logging channel %d\n", ch.number());
}

int main() {
    DmaChannel tx;
    log_channel(tx);
    std::printf("back in main\n");
    return 0;
}

channel 0 claimed
logging channel 0
back in main
channel 0 released

Three rules, in one table

Rule What it says In firmware
Rule of zero If every member looks after itself, write none of the special functions Most classes: pins, timeouts, buffers built from std::array
Rule of three If you write a destructor, decide about the copy constructor and copy assignment too Guards and owners: delete both copies
Rule of five The rule of three, plus the two move functions C++11 added Owners that must be handed on, covered in Volume 07

The rule of zero is the one to aim for, and it works because the compiler builds a class's copying out of its members' copying. A class that holds a DmaChannel cannot be copied either, without writing a single line about it:


// rule_of_zero.cpp - a class that writes none of the special functions
#include <array>
#include <cstdint>

class DmaChannel {
public:
    DmaChannel();
    ~DmaChannel();
    DmaChannel(const DmaChannel &) = delete;
    DmaChannel &operator=(const DmaChannel &) = delete;

private:
    int ch_;
};

// Rule of zero: its members look after themselves, so it declares no special functions.
class UartReceiver {
private:
    DmaChannel dma_;
    std::array<uint8_t, 64> buffer_;
};

void start() {
    UartReceiver rx;
    UartReceiver copy = rx;
}

rule_of_zero.cpp: In function 'void start()':
rule_of_zero.cpp:25:25: error: use of deleted function 'UartReceiver::UartReceiver(const UartReceiver&)'
rule_of_zero.cpp:17:7: note: 'UartReceiver::UartReceiver(const UartReceiver&)' is implicitly deleted because the default definition would be ill-formed:

The class UartReceiver says nothing about copying, yet copying it is refused, because one of its members cannot be copied. Its std::array member, on the other hand, would copy correctly on its own. Build classes from members that already behave correctly, and the whole class behaves correctly with no extra code.

Quick check

A class releases a DMA channel in its destructor, and has no other special functions. What goes wrong if an object is passed by value?

Show the answer

Answer: D. The compiler-written copy constructor copies the channel number, so two objects believe they own channel 0. Each destructor releases it, and double_release.cpp printed "released twice". Delete the copy functions, or pass a reference.

What you learned

Key words from this volume

Every word below has a plain-English entry in the glossary.

Practice

Practice 1

Find the leak

This C function adds a value to a small queue with interrupts off. One way out forgets to put interrupts back. Find it, then rewrite the function with an InterruptLock.


static bool push_c(uint32_t value) {
    uint32_t saved = get_primask();
    disable_irq();
    if (value == 0) {
        set_primask(saved);
        return false;
    }
    if (count == 4) {
        return false;                               // the queue is full - and interrupts stay off
    }
    queue[count] = value;
    count = count + 1;
    set_primask(saved);
    return true;
}
Show the solution

The second early return, taken when the queue is full, never calls set_primask. With a guard, no way out needs to remember:


static bool push(uint32_t value) {
    InterruptLock lock;
    if (value == 0 || count == 4) {
        return false;
    }
    queue[count] = value;
    count = count + 1;
    return true;
}

The course's test pushed 5, 0, 7, 8, 9 and 10 through both versions:


push_c( 5): true  interrupts on
push_c( 0): false interrupts on
push_c( 7): true  interrupts on
push_c( 8): true  interrupts on
push_c( 9): true  interrupts on
push_c(10): false interrupts off
push( 5):   true  interrupts on
push( 0):   false interrupts on
push( 7):   true  interrupts on
push( 8):   true  interrupts on
push( 9):   true  interrupts on
push(10):   false interrupts on

The fifth push filled the queue, so the sixth took the forgotten path, and the C version returned with interrupts off. The C++ version never can.

Practice 2

The lock that did nothing

A colleague protects a shared counter with InterruptLock(); on the first line of a function, then updates the counter. Testing on the board shows the update is still sometimes interrupted. Why?

Show the solution

The line makes a temporary object with no name. Its constructor switches interrupts off, and its destructor puts them back at the end of that same statement. That happens before the counter is touched. The program temporary.cpp showed the same order: "lock taken", "lock released", then the work. Give the guard a name: InterruptLock lock;.

Practice 3

Copy or not?

For each class, say whether copying should be allowed, and which rule applies.

  1. A Reading struct holding a timestamp and a temperature.
  2. An InterruptLock.
  3. A RxBuffer class holding a std::array and two indexes.
  4. A DmaChannel that releases its channel in its destructor.
Show the solution
  1. Allowed - copies of plain data are exactly what you want. Rule of zero: write nothing.
  2. Not allowed - a copy would put the interrupt state back twice. Delete both copy functions.
  3. Allowed, if a copied buffer makes sense - every member copies correctly. Rule of zero.
  4. Not allowed - two objects would release one channel. Delete both copy functions, and pass references. Handing the channel on is a move, which Volume 07 covers.

Interview corner

Interview question 1

What is RAII?

"What is RAII, and why does it matter in firmware?"

Show the solution

"RAII ties a resource to an object's lifetime: the constructor takes it and the destructor gives it back. The compiler runs the destructor on every way out of the scope, so early returns cannot leak the resource. In firmware I use it for interrupt locks, chip selects, bus locks and peripheral clocks. With exceptions switched off it costs no more than the release calls I would write by hand, and it can cost less."

Interview question 2

A critical-section guard

"How would you write a critical-section guard for a Cortex-M?"

Show the solution

"A class whose constructor saves PRIMASK and then disables interrupts, and whose destructor writes the saved value back. It must not simply enable interrupts, because critical sections nest and the caller may already have had them off. I would delete the copy constructor and copy assignment, so a copy cannot restore the state twice. And I would keep each guard in the smallest block that needs it, because everything inside adds to interrupt latency."

Interview question 3

The rule of three

"Explain the rule of three."

Show the solution

"If a class needs a user-written destructor, it almost certainly needs its copy constructor and copy assignment dealt with too. The compiler's versions copy members one by one, so two objects end up owning the same resource, and both destructors release it. The fix is to write proper copies or, far more often in firmware, to delete them. The rule of five adds the move functions, and the rule of zero says: build classes from members that manage themselves, and write none of these."