Volume 04 Intermediate 5 sub-modules ~20 min read

Inheritance and Polymorphism

Inheritance lets several classes share one set of code, and virtual functions let one piece of code drive many kinds of hardware. Both are useful in firmware, and one of them has a cost. This volume shows that cost in the machine code, builds a driver interface with it, and ends with CRTP, which gets the same sharing with no cost at all.

You will learn
  • How a derived class builds on a base class, and the order their parts are made in
  • What virtual does, what the vtable and the hidden pointer cost, and how override protects you
  • How to write an interface for a driver, and swap the hardware for a test port
  • When a virtual call is wasted, and how final lets the compiler remove it
  • CRTP: polymorphism decided while compiling, with no vtable
You need
  • Volumes 02 and 03 of this course: classes, constructors and destructors.

4.1 Inheritance

Inheritance builds a new class from an existing one. The new class gets all of the old one's members and adds its own, so code that every sensor needs is written once.

Every sensor in a system has a few things in common: a name, and a count of how many readings it has taken. In C you would copy those fields into each sensor's struct, or keep a common struct as the first member. In C++, a base class holds what they share, and each derived class adds its own part.


// inherit.cpp - what every sensor shares goes in a base class
#include <cstdint>
#include <cstdio>

class Sensor {
public:
    explicit Sensor(const char *name) : name_(name) {}
    const char *name() const { return name_; }
    uint32_t readings() const { return readings_; }

protected:
    void count_reading() { readings_ = readings_ + 1; }     // for derived classes to call

private:
    const char *name_;
    uint32_t readings_ = 0;
};

class Thermometer : public Sensor {
public:
    Thermometer() : Sensor("thermometer") {}
    int16_t read_celsius() {
        count_reading();
        return 25;
    }
};

class Barometer : public Sensor {
public:
    Barometer() : Sensor("barometer") {}
    uint16_t read_hpa() {
        count_reading();
        return 1013;
    }
};

int main() {
    Thermometer t;
    Barometer b;
    t.read_celsius();
    int16_t celsius = t.read_celsius();
    uint16_t hpa = b.read_hpa();
    std::printf("%s: %d C, readings so far %u\n", t.name(), celsius, static_cast<unsigned>(t.readings()));
    std::printf("%s: %u hPa, readings so far %u\n", b.name(), static_cast<unsigned>(hpa),
                static_cast<unsigned>(b.readings()));
    std::printf("sizes: Sensor %zu, Thermometer %zu, Barometer %zu bytes\n", sizeof(Sensor),
                sizeof(Thermometer), sizeof(Barometer));
    return 0;
}

thermometer: 25 C, readings so far 2
barometer: 1013 hPa, readings so far 1
sizes: Sensor 16, Thermometer 16, Barometer 16 bytes

The line class Thermometer : public Sensor says "a Thermometer is a Sensor, with more". So a Thermometer has name() and readings() without writing them again. Its constructor passes the name up to the Sensor constructor, in its member initialiser list: : Sensor("thermometer").

The word protected is new. A protected member, like count_reading(), may be used by the class and by classes derived from it, but not by other code. The data itself stays private: even a derived class must go through count_reading() to change the count.

All three classes are 16 bytes. A derived object is its base part plus its own data, and these two add functions but no data. Figure 4.1 shows what each object holds.

A derived object contains a whole base object a Thermometer object Sensor part name_, readings_ + read_celsius() a Barometer object Sensor part name_, readings_ + read_hpa() Same Sensor part in both; each class adds its own functions on top.
Figure 4.1 - Each Thermometer and each Barometer holds a complete Sensor part - its name and its count - and adds its own functions on top. The functions take no room in the object, so all three classes are 16 bytes on this PC.

The parts are made in a fixed order: the base part first, then the derived part. Destruction runs the other way, as with the objects in a block in Volume 02:


// order.cpp - the base part is built first and destroyed last
#include <cstdio>

class Sensor {
public:
    Sensor() { std::printf("Sensor part: constructed\n"); }
    ~Sensor() { std::printf("Sensor part: destroyed\n"); }
};

class Thermometer : public Sensor {
public:
    Thermometer() { std::printf("Thermometer part: constructed\n"); }
    ~Thermometer() { std::printf("Thermometer part: destroyed\n"); }
};

int main() {
    {
        Thermometer t;
        std::printf("using the thermometer\n");
    }
    return 0;
}

Sensor part: constructed
Thermometer part: constructed
using the thermometer
Thermometer part: destroyed
Sensor part: destroyed

A derived class does not get past the base class's private. If Thermometer tries to change readings_ itself, the compiler stops it, just as it would stop any other code:


// private_base.cpp - a derived class reaching for the base class's private data
#include <cstdint>

class Sensor {
public:
    uint32_t readings() const { return readings_; }

private:
    uint32_t readings_ = 0;
};

class Thermometer : public Sensor {
public:
    int read_celsius() {
        readings_ = readings_ + 1;
        return 25;
    }
};

private_base.cpp: In member function 'int Thermometer::read_celsius()':
private_base.cpp:15:9: error: 'uint32_t Sensor::readings_' is private within this context
private_base.cpp:9:14: note: declared private here
Common mistake

Using inheritance where one thing merely has another. A UART driver has a receive buffer; it is not a kind of buffer. Deriving Uart from RingBuffer would give it push() and pop() for anyone to call. Give it a buffer member instead. This is called composition, and it is the right choice far more often than inheritance.

Quick check

In inherit.cpp, how does Thermometer add to the reading count, when readings_ is private in Sensor?

Show the answer

Answer: C. Private members are closed to derived classes too, as private_base.cpp shows. The protected function count_reading() is the door Sensor leaves open for them, and the output shows the count reaching 2.

4.2 Virtual functions and the vtable cost

A virtual function lets a call through a base-class reference run the version belonging to the object's real class. The price is a hidden pointer in every object, a table of addresses per class, and a call the compiler cannot see through.

The problem virtual solves

Write a function that takes any Sensor, and call read() through it. Without virtual, the compiler decides which read() to call from the type it can see - Sensor - and never looks at the real object:


// static_binding.cpp - without virtual, a Sensor reference calls Sensor's read()
#include <cstdio>

class Sensor {
public:
    int read() const { return 0; }                  // "no reading"
};

class Thermometer : public Sensor {
public:
    int read() const { return 25; }
};

static void report(const Sensor &s) { std::printf("report() sees %d\n", s.read()); }

int main() {
    Thermometer t;
    std::printf("t.read() gives %d\n", t.read());
    report(t);
    return 0;
}

t.read() gives 25
report() sees 0

Now mark read() as virtual in the base class, and mark each replacement with override:


// virtual.cpp - with virtual, the object's real class decides which read() runs
#include <cstdio>

class Sensor {
public:
    virtual ~Sensor() = default;
    virtual int read() const { return 0; }
};

class Thermometer : public Sensor {
public:
    int read() const override { return 25; }
};

class Barometer : public Sensor {
public:
    int read() const override { return 1013; }
};

class Plain {
public:
    int read() const { return 0; }
};

static void report(const Sensor &s) { std::printf("report() sees %d\n", s.read()); }

int main() {
    Thermometer t;
    Barometer b;
    report(t);
    report(b);

    Sensor *all[] = {&t, &b};           // one list, two kinds of sensor
    int total = 0;
    for (Sensor *s : all) {
        total += s->read();
    }
    std::printf("total of all sensors: %d\n", total);
    std::printf("a class with no data: %zu byte without virtual, %zu bytes with it\n", sizeof(Plain),
                sizeof(Sensor));
    return 0;
}

report() sees 25
report() sees 1013
total of all sensors: 1038
a class with no data: 1 byte without virtual, 8 bytes with it

The same report() now reads each sensor properly, and one list can hold both kinds. Choosing the function while the program runs, from the object's real class, is called dynamic dispatch. One piece of code serving many types is polymorphism.

How it works, and what it costs

The last line shows the first cost. A class with no data at all grew from 1 byte to 8, because every object of a class with virtual functions carries a hidden pointer, the vptr. It points at the class's vtable, a table holding the address of each virtual function. The size harness looked inside one:


vtable for Thermometer: 40 bytes
  +0   0
  +8   0
  +16  Thermometer::~Thermometer()
  +24  Thermometer::~Thermometer()
  +32  Thermometer::read()
the object thermometer is 8 bytes, and holds: vtable for Thermometer+0x10

The first two words are bookkeeping the compiler needs. With RTTI switched off they hold nothing. Then come the addresses of the virtual functions: two entries for the destructor, which the compiler keeps in two forms, and one for read(). The object itself holds only the address of entry +16. And here is what a virtual call compiles to:


read_any(Sensor const&): 6 bytes
    mov    (%rdi),%rax
    jmp    *0x10(%rax)

The first instruction loads the hidden pointer out of the object. The second jumps through the entry 16 bytes further on, +32, which is read(). Figure 4.2 follows those two steps.

A virtual call: object, hidden pointer, vtable, function the object thermometer hidden pointer 8 bytes on this PC vtable for Thermometer, read-only +0 0 +8 0 +16 ~Thermometer() +24 ~Thermometer() +32 read() holds vtable+16 the function it reaches Thermometer::read() return 25; mov (%rdi),%rax 1. read the hidden pointer out of the object jmp *0x10(%rax) 2. jump through the entry 16 bytes on: read()
Figure 4.2 - The object holds only a hidden pointer to entry +16 of its class's vtable. A virtual call loads that pointer, then jumps through the entry 16 bytes further on - here read(). The vtable is read-only data, so on a microcontroller it sits in flash, one per class.

So a virtual function costs three things, all measured here. There is a hidden pointer in every object: 8 bytes on this PC, 4 on a 32-bit chip. There is a vtable per class, 40 bytes each for these small classes. And there is an indirect call, which the compiler cannot inline. None of that matters for a driver called a few times a second. All of it can matter in a loop that runs a million times, which is what the fourth sub-module is about.

Two safety nets

The keyword override checks that the function really does replace a virtual one. Forget the const, and the "replacement" would be a different function that nothing calls through the base. With override, the mistake stops the build:


// override_typo.cpp - override catches a function that does not match
class Sensor {
public:
    virtual ~Sensor() = default;
    virtual int read() const { return 0; }
};

class Thermometer : public Sensor {
public:
    int read() override { return 25; }          // const forgotten
};

override_typo.cpp:10:9: error: 'int Thermometer::read()' marked 'override', but does not override

Without override, the course's -Woverloaded-virtual flag still catches it, from the other side. It warns that the base version has been hidden, which is called name hiding:


// hidden.cpp - the same slip without override, caught by -Woverloaded-virtual
class Sensor {
public:
    virtual ~Sensor() = default;
    virtual int read() const { return 0; }
};

class Thermometer : public Sensor {
public:
    int read() { return 25; }                   // const forgotten, and no override
};

hidden.cpp:5:17: error: 'virtual int Sensor::read() const' was hidden [-Werror=overloaded-virtual=]
hidden.cpp:10:9: note:   by 'int Thermometer::read()'

The second net is the destructor. Every class with virtual functions here has virtual ~Sensor() = default;. If an object were ever destroyed through a pointer to its base class, a destructor that is not virtual would run only the base part's clean-up. The course's -Wnon-virtual-dtor flag insists on it:


// no_virtual_dtor.cpp - virtual functions, but a destructor that is not virtual
class Sensor {
public:
    virtual int read() const { return 0; }
};

no_virtual_dtor.cpp:2:7: error: 'class Sensor' has virtual functions and accessible non-virtual destructor [-Werror=non-virtual-dtor]
Remember

Mark every replacement override, and give every class with virtual functions a virtual destructor. Both cost nothing, and both turn silent bugs into build errors.

Quick check

What does the hidden pointer inside an object with virtual functions point at?

Show the answer

Answer: B. The harness showed the object thermometer holding "vtable for Thermometer+0x10". There is one vtable per class, in read-only data, and every Thermometer's hidden pointer points into it. A virtual call reads the pointer, then jumps through the table.

4.3 Interfaces for drivers

An interface is an abstract class with only pure virtual functions and no data. Driver code written against the interface works with any hardware behind it - including a fake one for tests.

A logger needs somewhere to send its bytes. On the board that is a UART. In a test on your PC it should be somewhere a test can look. If the logger is written against an interface, it never needs to know which:


// interface.cpp - a driver interface, two ports behind it, and code that uses either
#include <cstddef>
#include <cstdint>
#include <cstdio>

// The interface: what any serial port can do. It has no data, and no code of its own.
class SerialPort {
public:
    virtual ~SerialPort() = default;
    virtual void write(uint8_t byte) = 0;
};

// Code that needs a serial port, but does not care which one.
class Logger {
public:
    explicit Logger(SerialPort &port) : port_(port) {}
    void log(const char *text) {
        for (; *text != '\0'; text++) {
            port_.write(static_cast<uint8_t>(*text));
        }
        port_.write('\n');
    }

private:
    SerialPort &port_;
};

// On a chip this would wait for the UART's transmit register and store the byte there.
// On a PC it prints the byte, so the output can be seen.
class Uart1 : public SerialPort {
public:
    void write(uint8_t byte) override { std::putchar(byte); }
};

// A port for tests: it keeps every byte it is given, so a test can check them.
class CapturePort : public SerialPort {
public:
    void write(uint8_t byte) override {
        if (count_ < sizeof bytes_) {
            bytes_[count_] = byte;
            count_++;
        }
    }
    size_t count() const { return count_; }
    uint8_t byte(size_t i) const { return bytes_[i]; }

private:
    uint8_t bytes_[16] = {};
    size_t count_ = 0;
};

int main() {
    Uart1 uart;
    Logger boot_log(uart);
    boot_log.log("boot ok");

    CapturePort capture;
    Logger test_log(capture);
    test_log.log("hi");
    std::printf("the capture port holds %zu bytes:", capture.count());
    for (size_t i = 0; i < capture.count(); i++) {
        std::printf(" %02X", static_cast<unsigned>(capture.byte(i)));
    }
    std::printf("\n");
    return 0;
}

boot ok
the capture port holds 3 bytes: 68 69 0A

The = 0 after write makes it a pure virtual function: it has no body, and every class derived from SerialPort must supply one. A class with a pure virtual function is an abstract class. No object of it can be made on its own:


// abstract.cpp - an interface cannot be made on its own
#include <cstdint>

class SerialPort {
public:
    virtual ~SerialPort() = default;
    virtual void write(uint8_t byte) = 0;
};

void setup() {
    SerialPort port;
}

abstract.cpp: In function 'void setup()':
abstract.cpp:11:16: error: cannot declare variable 'port' to be of abstract type 'SerialPort'
abstract.cpp:4:7: note:   because the following virtual functions are pure within 'SerialPort':
abstract.cpp:7:18: note:     'virtual void SerialPort::write(uint8_t)'

The same check protects you when an interface grows. Add a read() to SerialPort, forget to write it in one of the ports, and that port cannot be made any more - the compiler lists what is missing:


// missing_function.cpp - a port that forgot one of the interface's functions
#include <cstdint>

class SerialPort {
public:
    virtual ~SerialPort() = default;
    virtual void write(uint8_t byte) = 0;
    virtual bool read(uint8_t &byte) = 0;
};

class Uart2 : public SerialPort {
public:
    void write(uint8_t byte) override { (void)byte; }
};

Uart2 uart2;

missing_function.cpp:16:7: error: cannot declare variable 'uart2' to be of abstract type 'Uart2'
missing_function.cpp:11:7: note:   because the following virtual functions are pure within 'Uart2':
missing_function.cpp:8:18: note:     'virtual bool SerialPort::read(uint8_t&)'

This is the start of a driver layer. Volume 08 builds a whole one on interfaces like SerialPort, and Volume 09 uses ports like CapturePort to test firmware on a PC.

In plain words

An interface is a promise with no work behind it: "anything that is a SerialPort can take a byte". The classes behind it keep the promise, each in its own way, and the code in front of it relies only on the promise.

Quick check

Why can Logger send bytes to both the UART and the capture port without any change to its code?

Show the answer

Answer: A. Logger only knows the interface. Each call to port_.write() goes through the vtable of the real object, so the UART prints the byte and the capture port stores it. Logger never asks which is which.

4.4 When to avoid virtual

A virtual call is only worth its cost when the choice really is made while the program runs. When the type is known while compiling, a direct call is cheaper, and it lets the compiler do the work in advance.

The harness compiled the same one-line job four ways. Here is each function's machine code:


read_any(Sensor const&): 6 bytes
    mov    (%rdi),%rax
    jmp    *0x10(%rax)
read_final(FinalThermometer const&): 6 bytes
    mov    $0x19,%eax
    ret
read_plain(PlainThermometer const&): 6 bytes
    mov    $0x19,%eax
    ret
read_crtp(CrtpThermometer const&): 6 bytes
    mov    $0x19,%eax
    ret

The byte counts are equal, but the work is not. The virtual version is only the first half of the job: it jumps into the real read(), which then has to run. The other three have already done all of it. The number 0x19 is 25, so each one simply returns the answer. The compiler could see which read() would run, so it put the function's body straight into the caller, and then worked out the result.

The second one is worth a closer look, because it still uses a virtual function:


struct FinalThermometer final : Sensor {
    int read() const override { return 25; }
};
int read_final(const FinalThermometer &t) { return t.read(); }

The keyword final says no class may be derived from FinalThermometer. So a FinalThermometer reference can only ever mean a FinalThermometer, and the compiler turned the virtual call back into a direct one. That is called devirtualisation.

When to use virtual, and when not

Situation Choose
Several kinds of object in one list, chosen while running Virtual functions
Hardware swapped for a fake in tests, and speed does not matter An interface with virtual functions
Exactly one implementation in each build A plain class, no virtual
Shared code, types all known while compiling, and speed matters CRTP, in the next sub-module
A virtual class you know the exact type of Mark it final
Common mistake

Making every function virtual "in case". Each one adds an entry to the vtable and a hidden pointer to every object, and blocks the compiler from inlining it. In an interrupt handler or a tight loop that is time you can measure. Add virtual only where a caller really needs to choose while running.

Quick check

Why did read_final compile to "return 25" when read is a virtual function?

Show the answer

Answer: D. With final, a FinalThermometer reference cannot refer to anything but a FinalThermometer. The compiler could therefore call FinalThermometer::read directly, put its body inline and return 25 - the same machine code as the plain version.

4.5 CRTP: static polymorphism

CRTP shares code through a base class that is told the derived class while compiling. There is no vtable and no hidden pointer, and every call is direct. That is static polymorphism.

The name stands for "curiously recurring template pattern", and the curious part is the first line of each sensor below: a class inherits from a template of itself. Templates are Volume 05's subject; for now, read SensorBase<Thermometer> as "a SensorBase that knows it is really part of a Thermometer".


// crtp.cpp - shared code in a base class, with the choice made while compiling
#include <cstdint>
#include <cstdio>

// The base is a template: Derived is filled in by each class that inherits from it.
template <typename Derived>
class SensorBase {
public:
    int32_t read_scaled() const {
        const Derived &self = static_cast<const Derived &>(*this);      // "I am really a Derived"
        return self.read_raw() * self.scale();
    }
};

class Thermometer : public SensorBase<Thermometer> {
public:
    int32_t read_raw() const { return 25; }
    int32_t scale() const { return 100; }       // hundredths of a degree
};

class Barometer : public SensorBase<Barometer> {
public:
    int32_t read_raw() const { return 1013; }
    int32_t scale() const { return 10; }        // tenths of a hectopascal
};

// Works with any class built on SensorBase - checked while compiling, not while running.
template <typename Derived>
static void report(const char *name, const SensorBase<Derived> &s) {
    std::printf("%s: %d\n", name, static_cast<int>(s.read_scaled()));
}

int main() {
    Thermometer t;
    Barometer b;
    report("thermometer, in hundredths of a degree", t);
    report("barometer, in tenths of a hectopascal", b);
    std::printf("a Thermometer is %zu byte: no hidden vtable pointer\n", sizeof t);
    return 0;
}

thermometer, in hundredths of a degree: 2500
barometer, in tenths of a hectopascal: 10130
a Thermometer is 1 byte: no hidden vtable pointer

The shared code, read_scaled(), lives once in the base. When it runs, it turns *this into the derived class with a static_cast. That is safe here, because a SensorBase<Thermometer> only ever exists as part of a Thermometer. From then on every call is an ordinary, direct call. The earlier machine code showed the result: read_crtp compiled to "return 25", the same as the plain class.

What CRTP gives up

Virtual functions CRTP
Hidden pointer per object Yes No
vtable per class Yes No
Calls can be inlined Only if devirtualised Yes
Mixed types in one list Yes No: each SensorBase<X> is a different type
Code size One copy of shared code One copy of shared code per derived class

The fourth row is the real trade. With CRTP there is no common base type, so an array of "any sensor" is not possible. Use CRTP when the types are all known while compiling and speed matters; use virtual functions when the choice truly happens while the program runs.

Quick check

In crtp.cpp, how does SensorBase::read_scaled() reach the derived class's read_raw()?

Show the answer

Answer: C. The base knows Derived from its template argument, so static_cast turns *this into a Derived reference while compiling. The call to read_raw() is then an ordinary direct call, with no vtable and no hidden pointer - a Thermometer is just 1 byte.

What you learned

Key words from this volume

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

Practice

Practice 1

Add a sensor

Here is an interface with one sensor behind it, and a function that reports any list of sensors. Add a Hygrometer that reads 40 and gives its unit as "% humidity". What has to change in report_all()?


class Sensor {
public:
    virtual ~Sensor() = default;
    virtual int read() const = 0;
    virtual const char *unit() const = 0;
};

static void report_all(const Sensor *const sensors[], int count) {
    for (int i = 0; i < count; i++) {
        std::printf("%d %s\n", sensors[i]->read(), sensors[i]->unit());
    }
}
Show the solution

class Hygrometer : public Sensor {
public:
    int read() const override { return 40; }
    const char *unit() const override { return "% humidity"; }
};

Nothing in report_all() changes - that is the point of the interface. The course's test put a thermometer and a hygrometer in one list:


25 C
40 % humidity
Practice 2

Which function runs?

What does this program print?


// practice_predict.cpp - practice: which function runs?
#include <cstdio>

class Sensor {
public:
    virtual ~Sensor() = default;
    virtual int read() const { return 1; }
    int id() const { return 10; }                   // not virtual
};

class Thermometer : public Sensor {
public:
    int read() const override { return 2; }
    int id() const { return 20; }                   // hides Sensor::id(), but does not override it
};

int main() {
    Thermometer t;
    const Sensor &s = t;
    std::printf("through t: read %d, id %d\n", t.read(), t.id());
    std::printf("through s: read %d, id %d\n", s.read(), s.id());
    return 0;
}
Show the solution

through t: read 2, id 20
through s: read 2, id 10

The virtual read() follows the real object, so both lines give 2. The plain id() follows the type the compiler can see: through t it is a Thermometer, giving 20, and through s it is a Sensor, giving 10. The course flags do not complain here, because -Woverloaded-virtual only watches virtual functions.

Practice 3

Virtual or CRTP?

Choose virtual functions or CRTP for each, and say why.

  1. A data logger that writes to whichever storage the user picks in a menu: SD card or internal flash.
  2. A motor-control loop running at 20 kHz, built for one motor driver chip per product.
  3. A test that swaps the real SPI bus for a fake that records transfers.
Show the solution
  1. Virtual functions. The choice is made while the program runs, from the user's menu.
  2. CRTP, or simply a plain class. The type is fixed for each build, and at 20 kHz an indirect call that cannot be inlined is a cost worth avoiding.
  3. Either works. An interface with virtual functions is simplest, and the indirect call does not matter in a test. CRTP, or passing the bus type as a template parameter, avoids the cost in the real build too.

Interview corner

Interview question 1

What does a virtual call cost?

"What is a vtable, and what does a virtual function cost on a microcontroller?"

Show the solution

"Each class with virtual functions gets a vtable: a read-only table of function addresses. Each object gets a hidden pointer to it - 4 bytes on a 32-bit chip. A virtual call loads that pointer, then jumps through the table, so it is indirect and cannot be inlined unless the compiler can prove the type, for example with final. On a PC, I measured a 40-byte vtable for a small class and a two-instruction call sequence. It is fine for drivers, but I avoid it in tight loops and interrupt handlers."

Interview question 2

Why a virtual destructor?

"Why should a base class with virtual functions have a virtual destructor?"

Show the solution

"If an object is ever destroyed through a pointer to its base class, a non-virtual destructor would run only the base part's clean-up, and the derived part's resources would leak. That is undefined behaviour. In firmware objects are rarely destroyed that way, but the fix costs nothing, and -Wnon-virtual-dtor enforces it in the build."

Interview question 3

What is CRTP?

"What is CRTP, and when would you use it in firmware?"

Show the solution

"The curiously recurring template pattern: a class inherits from a template of itself, like Thermometer deriving from SensorBase of Thermometer. The base can then cast itself to the derived type and call it directly. You get shared code without a vtable or a hidden pointer, and the calls can be inlined. I use it when all the types are known while compiling and speed matters. The price is that there is no common base type, so mixed objects cannot share one list."