Volume 02 Beginner 5 sub-modules ~25 min read

Classes and Objects

A class keeps some data and the functions that use it together, and lets only those functions change the data. This volume builds up to a real GPIO pin class, driven against the same registers as Embedded C. Along the way the machine code shows that none of it costs anything when the program runs.

You will learn
  • How public and private let a class keep its own rules
  • Why a member function compiles to the same code as a C function taking a struct pointer
  • When constructors and destructors run, and two traps in writing them
  • How to write GPIO pin classes that cannot drive an input by mistake
  • What const after a member function promises, and why const references need it
  • Static members, and the kind of member function that can be a C callback
You need
  • Volume 01 of this course, especially references.
  • The GPIO registers and BSRR from Embedded C from Zero, in its registers volume.

2.1 Classes and access control

A class keeps some data and the functions that use it together. Marking the data private means only those functions can change it, so the class can keep its own rules.

In C, a struct holds data, and any code with the struct in reach can change any member. Suppose a PWM channel's duty must stay between 0 and 100 percent. In C, that rule lives in a comment, and in the hope that every caller reads it.


// access.cpp - a class keeps its data and its rules together
#include <cstdint>
#include <cstdio>

// The C way: anyone can write any value into duty.
struct pwm_c {
    uint8_t duty;           // percent, 0 to 100 - but nothing makes sure of it
};

// The C++ way: duty_ can only change through set_duty(), which keeps the rule.
class PwmChannel {
public:
    void set_duty(uint8_t percent) {
        if (percent > 100) {
            percent = 100;
        }
        duty_ = percent;
    }
    uint8_t duty() { return duty_; }

private:
    uint8_t duty_ = 0;
};

int main() {
    pwm_c a;
    a.duty = 150;                       // compiles: nothing stops it
    std::printf("C struct:  duty = %u%%\n", static_cast<unsigned>(a.duty));

    PwmChannel b;
    b.set_duty(150);                    // the class applies its rule
    std::printf("C++ class: duty = %u%%\n", static_cast<unsigned>(b.duty()));

    std::printf("sizes: struct %zu byte, class %zu byte\n", sizeof a, sizeof b);
    return 0;
}

C struct:  duty = 150%
C++ class: duty = 100%
sizes: struct 1 byte, class 1 byte

The class has two parts:

The call b.set_duty(150) runs set_duty for the object b, and the function keeps the rule: anything above 100 becomes 100. The struct took 150 without a murmur. Both are 1 byte, because the functions are not stored inside the object.

A rule like "the duty is between 0 and 100" is called an invariant. Keeping the data private, so that only the class's functions can change it, is called encapsulation. The words public and private are access specifiers.

Think of it like this

Think of a vending machine. You can press its buttons, but you cannot reach inside and move the coins around. The buttons are the public member functions, and the coins are the private data. However the machine is used, its count of coins stays right.

When code outside the class tries to reach in anyway, the compiler stops it:


// private.cpp - code outside the class tries to write duty_ itself
#include <cstdint>

class PwmChannel {
    uint8_t duty_ = 0;              // no label yet, and a class starts out private
public:
    void set_duty(uint8_t percent);
};

void overdrive(PwmChannel &pwm) {
    pwm.duty_ = 150;
}

private.cpp: In function 'void overdrive(PwmChannel&)':
private.cpp:11:9: error: 'uint8_t PwmChannel::duty_' is private within this context
private.cpp:5:13: note: declared private here

struct or class?

In C++ the two words differ in one way only: where they start. A struct's members are public until you say otherwise, which is why a.duty = 150 compiled. A class's members are private until you say otherwise, as private.cpp shows. Both can have member functions.

By habit, struct is used for plain data with no rules to keep, such as a block of registers or a received message. The word class is used for a type that guards its data. This course follows that habit.

What a member function really is

A member function is compiled like an ordinary function with one hidden extra argument: the address of the object it was called on. Inside the function, that address is called this, and duty_ is short for this->duty_.

The course's size harness checked this. It compiled a C-style function that takes a struct pointer, and a member function that does the same job, and compared the machine code:


struct led_c {
    uint8_t pin;
    bool on;
};
void led_c_toggle(led_c *l) { l->on = !l->on; }

class Led {
public:
    void toggle();

private:
    uint8_t pin_;
    bool on_;
};
void Led::toggle() { on_ = !on_; }

led_c_toggle(led_c*):
    xorb   $0x1,0x1(%rdi)
    ret
Led::toggle():
    xorb   $0x1,0x1(%rdi)
    ret
-> the same 5 bytes of machine code, byte for byte

The instruction xorb $0x1,0x1(%rdi) flips the lowest bit of the byte one place past the address held in register rdi. That byte is on in the struct and on_ in the class. In both functions the object's address arrives in rdi: in the C version it is the parameter l, and in the C++ version it is this.

Notice also how toggle was defined outside its class: void Led::toggle(). The Led:: in front says which class the function belongs to, using the scope resolution operator from Volume 01.

Quick check

In access.cpp, why does the class print 100% when the struct prints 150%?

Show the answer

Answer: B. The member duty_ is private, so outside code can change it only by calling set_duty, and that function turns 150 into 100. The struct's member is public, so a.duty = 150 went straight in.

2.2 Constructors and destructors

A constructor runs when an object is made, and sets it up. A destructor runs when the object's life ends. The compiler calls both for you, at moments you can predict exactly.

In C, a struct is set up by hand, with a call such as uart_init(&uart, 9600), and nothing stops you using it before that call. In C++, the set-up code is the constructor, and an object cannot exist without it running.


// lifetime.cpp - when constructors and destructors run
#include <cstdio>

class Trace {
public:
    explicit Trace(const char *name) : name_(name) { std::printf("%s: constructed\n", name_); }
    ~Trace() { std::printf("%s: destroyed\n", name_); }

private:
    const char *name_;
};

Trace global("global");             // made before main() starts

int main() {
    std::printf("main starts\n");
    Trace a("a");
    Trace b("b");
    {
        Trace c("c");
        std::printf("end of the inner block\n");
    }
    std::printf("main returns\n");
    return 0;
}

global: constructed
main starts
a: constructed
b: constructed
c: constructed
end of the inner block
c: destroyed
main returns
b: destroyed
a: destroyed
global: destroyed

Three new pieces of syntax appear in Trace:

Now read the output in order. The global object was constructed before main started. Inside main, each object was constructed on the line that defines it. Each was destroyed at the closing brace of its block: c at the end of the inner block, then b and a when main returned. Figure 2.1 draws the same run as a timeline.

The lifetime of each object in lifetime.cpp before main() main() runs after main() returns global a b c the inner block Last made, first destroyed: c, then b, then a - and global last of all. constructed destroyed
Figure 2.1 - Each bar runs from the object's constructor to its destructor. The global object is made before main() starts and destroyed after it returns. Inside main(), the objects are destroyed in the reverse order of construction: c first, then b, then a.
In plain words

A local object lives from the line that makes it to the closing brace of its block. That span is its lifetime. The compiler places the destructor call at every way out of the block, so it cannot be forgotten. Volume 03 builds a whole technique on exactly this.

On a microcontroller two details change. Global objects are constructed by the start-up code before main() is called. And main() usually never returns, so global objects are never destroyed at all.

No object without its constructor

Once a class has a constructor that needs a pin number, there is no way to make one without saying which pin:


// no_default.cpp - an object cannot skip its constructor
#include <cstdint>

class Led {
public:
    explicit Led(uint8_t pin) : pin_(pin) {}

private:
    uint8_t pin_;
};

Led status;                         // which pin? The compiler will not guess.

no_default.cpp:12:5: error: no matching function for call to 'Led::Led()'
no_default.cpp:6:14: note: candidate 1: 'Led::Led(uint8_t)'
no_default.cpp:6:14: note: candidate expects 1 argument, 0 provided

A constructor with no arguments is called a default constructor, and this class has none. So there is no such thing as a half-set-up Led. If you compile this yourself, g++ also lists two constructors that the compiler wrote by itself, for copying and moving. Volumes 03 and 07 explain those.

A class may also give a member a starting value right where it is declared, as PwmChannel did with uint8_t duty_ = 0;. This is a default member initialiser. Every constructor that does not set that member itself uses it. That is why PwmChannel b; in access.cpp started at 0 without anyone writing a constructor.

There is one thing a constructor cannot do: report an error. It has no return value, and with exceptions switched off it has no other way out. The GPIO class in the next sub-module shows one answer, and Volume 06 covers the rest.

Trap 1: members are set in the order they are declared


// reorder.cpp - the initialiser list in one order, the members in another
#include <cstdint>

class Uart {
public:
    explicit Uart(uint32_t baud) : baud_(baud), divisor_(16000000u / (16u * baud_)) {}
    uint32_t divisor() const { return divisor_; }

private:
    uint32_t divisor_;              // declared first, so it is set first
    uint32_t baud_;
};

uint32_t make_uart() {
    Uart u(9600);
    return u.divisor();
}

reorder.cpp: In constructor 'Uart::Uart(uint32_t)':
reorder.cpp:11:14: error: 'Uart::baud_' will be initialized after [-Werror=reorder]
reorder.cpp:10:14: error:   'uint32_t Uart::divisor_' [-Werror=reorder]
reorder.cpp:6:14: error:   when initialized here [-Werror=reorder]
reorder.cpp:6:77: error: member 'Uart::baud_' is used uninitialized [-Werror=uninitialized]

The list reads as if baud_ is set first. It is not. Members are always set in the order the class declares them, so divisor_ comes first, and it is worked out from a baud_ that has no value yet. The -Wall warnings catch both halves of the problem, and -Werror stops the build. The fix is to declare the members in the order the constructor needs them, or to use the parameter baud rather than the member.

Trap 2: a constructor that converts silently


// implicit.cpp - a constructor without explicit lets a number turn into an object
#include <cstdint>
#include <cstdio>

class Led {
public:
    Led(uint8_t pin) : pin_(pin) {}         // no explicit: see what this allows
    uint8_t pin() const { return pin_; }

private:
    uint8_t pin_;
};

static void set_brightness(Led led, uint8_t percent) {
    std::printf("LED on pin %u at %u%%\n", static_cast<unsigned>(led.pin()), static_cast<unsigned>(percent));
}

int main() {
    Led status(5);
    set_brightness(status, 80);     // what was meant
    set_brightness(80, 5);          // arguments swapped: it still compiles
    return 0;
}

LED on pin 5 at 80%
LED on pin 80 at 5%

A constructor with one argument tells the compiler how to turn that argument into the class, and the compiler will do so whenever it helps a call to fit. Here it made a Led on pin 80 out of the number 80, and the swapped call compiled. The keyword explicit stops that:


// explicit.cpp - the same mistake, with an explicit constructor
#include <cstdint>

class Led {
public:
    explicit Led(uint8_t pin) : pin_(pin) {}
    uint8_t pin() const { return pin_; }

private:
    uint8_t pin_;
};

void set_brightness(Led led, uint8_t percent);

void dim() {
    set_brightness(80, 5);
}

explicit.cpp: In function 'void dim()':
explicit.cpp:16:20: error: could not convert '80' from 'int' to 'Led'
Remember

Mark every one-argument constructor explicit, unless you really want the conversion. Declare members in the order the constructor sets them.

Quick check

In lifetime.cpp, in what order are a and b destroyed when main returns?

Show the answer

Answer: B. The output shows b: destroyed before a: destroyed. Objects in a block are destroyed in the reverse order of construction, so the last one made is the first one cleaned up.

2.3 A GPIO pin class

A driver class hides the registers behind a few clear functions. Giving output pins and input pins separate classes lets the compiler refuse to drive a button.

Embedded C from Zero, Volume 10 wrote a GPIO driver as four C functions: gpio_set_dir, gpio_write, gpio_toggle and gpio_read. Each takes a pin number and checks it. Here is the same driver as two small classes. It runs against the same kind of fake port, so it runs on a PC.


// gpio_pin.cpp - the GPIO driver from Embedded C, as two small classes
#include <cstdint>
#include <cstdio>

// One GPIO port's registers, laid out as the datasheet shows them.
struct GpioPort {
    volatile uint32_t MODER;        // 0x00  two bits per pin: 00 input, 01 output
    volatile uint32_t OTYPER;       // 0x04
    volatile uint32_t OSPEEDR;      // 0x08
    volatile uint32_t PUPDR;        // 0x0C
    volatile uint32_t IDR;          // 0x10  what the pins are reading
    volatile uint32_t ODR;          // 0x14  what we are driving out
    volatile uint32_t BSRR;         // 0x18  write-only: set bits low, clear high
};

class OutputPin {
public:
    OutputPin(GpioPort &port, uint8_t pin) : port_(port), mask_(pin < 16 ? 1u << pin : 0u) {
        if (mask_ != 0) {                       // a bad pin gets mask 0, and touches nothing
            uint32_t shift = pin * 2u;
            port_.MODER = (port_.MODER & ~(3u << shift)) | (1u << shift);     // 01: output
        }
    }
    void set() { port_.BSRR = mask_; }          // one store: an interrupt cannot split it
    void clear() { port_.BSRR = mask_ << 16; }
    void toggle() {
        if (is_set()) {
            clear();
        } else {
            set();
        }
    }
    bool is_set() const { return (port_.ODR & mask_) != 0; }

private:
    GpioPort &port_;
    uint32_t mask_;
};

class InputPin {
public:
    InputPin(GpioPort &port, uint8_t pin) : port_(port), mask_(pin < 16 ? 1u << pin : 0u) {
        if (mask_ != 0) {
            uint32_t shift = pin * 2u;
            port_.MODER = port_.MODER & ~(3u << shift);                        // 00: input
        }
    }
    bool read() const { return (port_.IDR & mask_) != 0; }

private:
    GpioPort &port_;
    uint32_t mask_;
};

// ---- the fake chip: silicon does this by itself, so none of it is driver code ----
static GpioPort fake_port;          // on the chip: *reinterpret_cast<GpioPort *>(0x40020000u)

static void hw_settle() {
    fake_port.ODR |= fake_port.BSRR & 0xFFFFu;
    fake_port.ODR &= ~(fake_port.BSRR >> 16);
    fake_port.BSRR = 0;
}

static void report(const char *what) {
    std::printf("%-24s MODER=%08X  ODR=%04X\n", what, static_cast<unsigned>(fake_port.MODER),
                static_cast<unsigned>(fake_port.ODR));
}

int main() {
    OutputPin led(fake_port, 5);
    InputPin button(fake_port, 13);
    report("after making the pins");

    led.set();
    hw_settle();
    report("led.set()");
    led.toggle();
    hw_settle();
    report("led.toggle()");
    led.toggle();
    hw_settle();
    report("led.toggle() again");

    std::printf("button reads %s\n", button.read() ? "pressed" : "released");
    fake_port.IDR |= 1u << 13;          // someone presses the button
    std::printf("button reads %s\n", button.read() ? "pressed" : "released");

    OutputPin bad(fake_port, 99);       // refused: mask 0
    bad.set();
    hw_settle();
    report("after a pin 99");

    std::printf("an OutputPin is %zu bytes on this PC\n", sizeof led);
    return 0;
}

after making the pins    MODER=00000400  ODR=0000
led.set()                MODER=00000400  ODR=0020
led.toggle()             MODER=00000400  ODR=0000
led.toggle() again       MODER=00000400  ODR=0020
button reads released
button reads pressed
after a pin 99           MODER=00000400  ODR=0020
an OutputPin is 16 bytes on this PC

The numbers are the same as the C driver's. In MODER=00000400 only bit 10 is set, so pin 5 holds 01 (output) and pin 13 holds 00 (input). In ODR=0020 bit 5 is set, so the LED is on. Here is how the pieces map onto the C version:

Figure 2.2 shows what one pin object holds, and which registers its functions use.

One OutputPin object and the registers it drives OutputPin led(fake_port, 5); port_ mask_ = 0x00000020 16 bytes on this PC refers to its port fake_port MODER OTYPER OSPEEDR PUPDR IDR ODR BSRR 0x00 0x04 0x08 0x0C 0x10 0x14 0x18 constructor sets MODER bits 11:10 to 01 set() writes BSRR = mask_ clear() writes BSRR = mask_ << 16 is_set() reads ODR & mask_
Figure 2.2 - The object led holds two things: a reference to its port and a mask with bit 5 set. The constructor sets pin 5's two bits in MODER, set() and clear() each make one write to BSRR, and is_set() reads ODR. The other registers are never touched.

The compiler refuses to drive an input

In the C driver, a pin is just a number, so gpio_write(BUTTON, true) compiles without complaint. With two classes, an input pin has no set() to call:


// input_set.cpp - trying to drive a pin that was made as an input
#include <cstdint>

struct GpioPort {
    volatile uint32_t MODER, OTYPER, OSPEEDR, PUPDR, IDR, ODR, BSRR;
};

class InputPin {
public:
    InputPin(GpioPort &port, uint8_t pin) : port_(port), mask_(pin < 16 ? 1u << pin : 0u) {}
    bool read() const { return (port_.IDR & mask_) != 0; }

private:
    GpioPort &port_;
    uint32_t mask_;
};

void light_the_button(GpioPort &port) {
    InputPin button(port, 13);
    button.set();
}

input_set.cpp: In function 'void light_the_button(GpioPort&)':
input_set.cpp:20:12: error: 'class InputPin' has no member named 'set'

A constructor that cannot fail, and what it costs

A pin number of 16 or more makes the mask 0. Every function then writes a 0 to BSRR, which changes nothing, or reads through a 0 mask, which always gives false. That is the class's version of the C driver's guard clauses, and the "after a pin 99" line shows that nothing moved. It is one way round the problem from the last sub-module: the constructor cannot report the error, so it makes the object harmless instead.

An OutputPin is 16 bytes on this PC. The reference is stored as an 8-byte address and the mask takes 4. Then 4 more bytes of padding keep the object's size a whole number of 8-byte words. On a 32-bit microcontroller, where an address takes 4 bytes, there is no padding to add. On the chip, the port is not a variable but the registers themselves, reached with the reinterpret_cast from Volume 01, as the comment beside fake_port shows.

Common mistake

Writing one Pin class with set(), read() and a direction flag inside it. Then set() on an input compiles, and has to be caught by a check while the program runs - or not at all. Separate types move that check into the compiler, where it costs nothing.

Quick check

What happens if code calls set() on an InputPin?

Show the answer

Answer: B. The class InputPin simply has no set() function, so g++ reports "'class InputPin' has no member named 'set'". The mistake is caught while compiling, before the firmware ever reaches a board.

2.4 const member functions

A const member function promises not to change its object. The compiler holds it to that promise, and only such functions can be called on a const object or through a const reference.

Volume 01 said to pass structs by const reference. That raises a question. When a function receives a const PwmChannel &, which member functions may it call? Only the ones marked const.


// const_members.cpp - const member functions, const objects and const references
#include <cstdint>
#include <cstdio>

class PwmChannel {
public:
    void set_duty(uint8_t percent) {
        if (percent > 100) {
            percent = 100;
        }
        duty_ = percent;
    }
    uint8_t duty() const { return duty_; }      // const: this function only reads

private:
    uint8_t duty_ = 0;
};

// A const reference: report() may call only the const member functions.
static void report(const char *name, const PwmChannel &pwm) {
    std::printf("%s: duty %u%%\n", name, static_cast<unsigned>(pwm.duty()));
}

int main() {
    PwmChannel fan;
    fan.set_duty(40);
    report("fan", fan);

    const PwmChannel heater;        // a const object: it stays at its starting value
    report("heater", heater);
    return 0;
}

fan: duty 40%
heater: duty 0%

The word const after the brackets of duty() is a promise: this function only reads. So report(), which holds a const reference, may call duty(). It could not call set_duty(), and neither can anyone holding the const object heater, which stays at the 0 its default member initialiser gave it.

Leave the const off, and a function that only reads can no longer be used through a const reference:


// const_call.cpp - duty() forgot to say const
#include <cstdint>
#include <cstdio>

class PwmChannel {
public:
    uint8_t duty() { return duty_; }            // reads only, but does not say so

private:
    uint8_t duty_ = 0;
};

void report(const PwmChannel &pwm) {
    std::printf("duty %u%%\n", static_cast<unsigned>(pwm.duty()));
}

const_call.cpp: In function 'void report(const PwmChannel&)':
const_call.cpp:14:62: error: passing 'const PwmChannel' as 'this' argument discards qualifiers [-fpermissive]
const_call.cpp:7:13: note:   in call to 'uint8_t PwmChannel::duty()'

The message is worth learning to read. The object passed as this is const, and calling a function that is not marked const would throw that const away. "Qualifiers" is the standard's word for const and volatile.

The promise is checked from the inside too. A const member function that tries to change the object does not compile:


// const_write.cpp - a const member function that tries to change the object
#include <cstdint>

class PwmChannel {
public:
    uint8_t duty() const {
        duty_ = 0;                  // a "read" that quietly resets the channel
        return duty_;
    }

private:
    uint8_t duty_ = 0;
};

const_write.cpp: In member function 'uint8_t PwmChannel::duty() const':
const_write.cpp:7:15: error: assignment of member 'PwmChannel::duty_' in read-only object

Look back at access.cpp in the first sub-module. Its duty() was not marked const. Nothing complained, because nothing const ever called it, but it was a mistake waiting for its first const reference. The GPIO classes got it right: is_set() and read() are both const.

Remember

If a member function does not change the object, write const after its brackets. It costs nothing, and without it the function cannot be used through a const reference.

Quick check

A function takes const PwmChannel &pwm and calls pwm.set_duty(50). What happens?

Show the answer

Answer: B. The function set_duty changes the object, so it is not const. Through a const reference only const member functions may be called. The compiler reports "passing 'const PwmChannel' as 'this' argument discards qualifiers", as it did for const_call.cpp.

2.5 Static members

A static member belongs to the class, not to any one object. A static variable has one copy shared by every object. A static function needs no object at all, and that is what lets it be a C callback.


// static_members.cpp - one copy for the whole class, and functions with no object
#include <cstdint>
#include <cstdio>

class Sensor {
public:
    Sensor() { count_++; }
    ~Sensor() { count_--; }
    static uint32_t count() { return count_; }      // no object needed to ask

private:
    static inline uint32_t count_ = 0;              // one copy, shared by every Sensor
    uint16_t last_reading_ = 0;                     // one copy inside each Sensor
};

// The kind of callback a C driver takes: a plain function, no object.
using callback_t = void (*)();

class Ticker {
public:
    static void on_tick() { ticks_ = ticks_ + 1; }  // no object, so it fits callback_t
    static uint32_t ticks() { return ticks_; }

private:
    static inline volatile uint32_t ticks_ = 0;     // changed from an interrupt
};

extern "C" void TIM2_IRQHandler() { Ticker::on_tick(); }

int main() {
    std::printf("Sensors before any exist: %u\n", static_cast<unsigned>(Sensor::count()));
    Sensor a, b;
    {
        Sensor c;
        std::printf("Sensors inside the block: %u\n", static_cast<unsigned>(Sensor::count()));
    }
    std::printf("Sensors after the block:  %u\n", static_cast<unsigned>(Sensor::count()));
    std::printf("a Sensor is %zu bytes: the count is not stored inside it\n", sizeof a);

    callback_t cb = &Ticker::on_tick;               // a static member function is a plain function
    cb();
    TIM2_IRQHandler();                              // standing in for two timer interrupts
    TIM2_IRQHandler();
    std::printf("ticks: %u\n", static_cast<unsigned>(Ticker::ticks()));
    return 0;
}

Sensors before any exist: 0
Sensors inside the block: 3
Sensors after the block:  2
a Sensor is 2 bytes: the count is not stored inside it
ticks: 3

One copy for the whole class

There is only one count_, shared by every Sensor. Each constructor adds one and each destructor takes one away, so count() always says how many sensors exist. There were none at the start and three inside the block. After it there were two, because c had been destroyed at the closing brace. Each Sensor is 2 bytes, which is just last_reading_: the count is kept once, outside the objects.

A static member function is called on the class, not on an object, as in Sensor::count(). The word inline in static inline uint32_t count_ = 0; lets the variable be defined right there in the class. Older code defines it once in a .cpp file instead.

A static function as a callback

A C driver that calls you back usually wants a plain function pointer, like callback_t here. A static member function has no this, so it is a plain function, and its address fits. The interrupt handler uses the same fact. It has a plain C name, so the vector table can find it, and all it does is pass the work to the class. That is a common shape for interrupt handlers in C++.

An ordinary member function does not fit, because it cannot run without an object to work on:


// member_callback.cpp - an ordinary member function passed as a plain callback
class Blinker {
public:
    void toggle() { on_ = !on_; }

private:
    bool on_ = false;
};

using callback_t = void (*)();
void timer_set_callback(callback_t cb);

void setup() {
    timer_set_callback(&Blinker::toggle);
}

member_callback.cpp: In function 'void setup()':
member_callback.cpp:14:24: error: cannot convert 'void (Blinker::*)()' to 'callback_t' {aka 'void (*)()'}

The type void (Blinker::*)() is a pointer to a member function: it needs a Blinker to be called on. The other side of the same coin is that a static member function cannot touch anything that belongs to one object, because it has no object:


// static_this.cpp - a static member function has no object to look inside
#include <cstdint>

class Ticker {
public:
    static void on_tick() { elapsed_ms_ = elapsed_ms_ + period_ms_; }

private:
    uint32_t elapsed_ms_ = 0;
    uint32_t period_ms_ = 10;
};

static_this.cpp: In static member function 'static void Ticker::on_tick()':
static_this.cpp:6:29: error: invalid use of member 'Ticker::elapsed_ms_' in static member function
Common mistake

Writing ticks_++ on a volatile counter. It is the same line as the tick counter in Embedded C from Zero, Volume 11. But in C++20, ++ on a volatile variable is deprecated - still allowed, but marked for removal - and the course's build treats that warning as an error:


// volatile_increment.cpp - the tick counter from Embedded C, compiled as C++20
#include <cstdint>

volatile uint32_t tick_count = 0;

extern "C" void TIM2_IRQHandler() {
    tick_count++;
}

volatile_increment.cpp: In function 'void TIM2_IRQHandler()':
volatile_increment.cpp:7:5: error: '++' expression of 'volatile'-qualified type is deprecated [-Werror=volatile]

Write ticks_ = ticks_ + 1;, as static_members.cpp does. It does the same work, and it makes plain that this is a read followed by a separate write, not one step that nothing can interrupt. The |= in gpio_pin.cpp is accepted: here, it is ++ that the compiler objects to.

Quick check

Why can Ticker::on_tick be stored in a plain void (*)() callback, while Blinker::toggle cannot?

Show the answer

Answer: B. A static member function needs no object, so it is compiled as a plain function and fits a plain function pointer. The function Blinker::toggle needs a Blinker to work on, so its type is a pointer to a member function, which g++ refuses to convert.

What you learned

Key words from this volume

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

Practice

Practice 1

Make it a class

Rewrite this C timeout as a class called Timeout. The limit should be given when the object is made, and checking the time should not be able to change the object.


struct timeout_c {
    uint32_t start_ms;
    uint32_t limit_ms;
};
static void timeout_start(timeout_c *t, uint32_t now_ms, uint32_t limit_ms) {
    t->start_ms = now_ms;
    t->limit_ms = limit_ms;
}
static bool timeout_expired(const timeout_c *t, uint32_t now_ms) {
    return now_ms - t->start_ms >= t->limit_ms;
}
Show the solution

class Timeout {
public:
    explicit Timeout(uint32_t limit_ms) : limit_ms_(limit_ms) {}
    void start(uint32_t now_ms) { start_ms_ = now_ms; }
    bool expired(uint32_t now_ms) const { return now_ms - start_ms_ >= limit_ms_; }

private:
    uint32_t start_ms_ = 0;
    uint32_t limit_ms_;
};

The limit comes in through an explicit constructor, and expired() is const, because checking the time changes nothing. The course's test ran both versions side by side:


at 1200 ms: C waiting, C++ waiting
at 1500 ms: C expired, C++ expired
started at 0xFFFFFF00, now 0x00000050: 336 ms gone, waiting
started at 0xFFFFFF00, now 0x00000100: 512 ms gone, expired

The last two lines are worth a second look. The millisecond count wrapped round past zero, and the timeout still worked. That is because unsigned subtraction wraps round too, so now_ms - start_ms_ still gives the true time gone: 336 ms in the first case and 512 ms in the second.

Practice 2

Predict the order

Using the same Trace class as lifetime.cpp, what does this program print?


// practice_order.cpp - practice: predict the order of these lines
#include <cstdio>

class Trace {
public:
    explicit Trace(const char *name) : name_(name) { std::printf("%s: constructed\n", name_); }
    ~Trace() { std::printf("%s: destroyed\n", name_); }

private:
    const char *name_;
};

static void blink() {
    Trace t("t");
    std::printf("blink\n");
}

int main() {
    Trace a("a");
    blink();
    blink();
    Trace b("b");
    return 0;
}
Show the solution

a: constructed
t: constructed
blink
t: destroyed
t: constructed
blink
t: destroyed
b: constructed
b: destroyed
a: destroyed

The object t is local to blink(), so each call makes a fresh t and destroys it at the function's closing brace. The object b is made after both calls, so it is destroyed first when main returns.

Practice 3

A callback for one LED

A C timer driver takes a callback of type void (*)(). You want each timer tick to toggle one particular Blinker object, the status LED. As member_callback.cpp showed, &Blinker::toggle will not fit. How can you do it?

Show the solution

Write a plain function that toggles that one object, and pass that:


static Blinker status_led;                          // the object the callback works on
static void toggle_status_led() { status_led.toggle(); }

Then timer_set_callback(toggle_status_led); fits, because toggle_status_led is an ordinary function. The course's test called it three times and printed tick 1: LED on, tick 2: LED off and tick 3: LED on. A static member function would work just as well. Volume 07 shows a neater way to write the same thing, with a lambda.

Interview corner

Interview question 1

struct or class?

"What is the difference between a struct and a class in C++?"

Show the solution

"Only the starting access: a struct's members are public until you say otherwise, and a class's are private. Both can have member functions, constructors and destructors. I use struct for plain data with no rules, such as a register block or a message. I use class when the type has invariants to protect, such as a driver or a buffer."

Interview question 2

How does a member function know its object?

"How does a member function know which object it was called on, and what does that cost?"

Show the solution

"The compiler passes the object's address as a hidden first argument, called this. So a member function is compiled like a C function that takes a pointer to a struct. Measured with objdump, a toggle written both ways gave the same five bytes of machine code. It costs nothing extra; only virtual functions add a cost, and that is a separate feature."

Interview question 3

A constructor that cannot fail

"Without exceptions, how does a constructor report an error?"

Show the solution

"It can't: a constructor has no return value, and with -fno-exceptions it cannot throw. So you design around it. You can make the object harmless when its arguments are bad, as a GPIO pin class can with a zero mask. You can split the work into a constructor that cannot fail and an init() function that returns a status. Or you can use a factory function that returns an object only when the arguments are good. The choice depends on whether the caller must be forced to check."