Quick Cap
C++ classes give embedded developers type-safe hardware abstraction, automatic resource management via constructors/destructors, and compile-time polymorphism through templates -- all without necessarily paying runtime overhead. The key interview challenge is knowing which C++ features are free and which cost ROM, RAM, or predictability.
Interviewers use this topic to test whether you can leverage C++ effectively on resource-constrained targets without blindly importing desktop OOP patterns.
Key Facts:
- Classes vs C structs: A C++ class with no virtual functions compiles to the same machine code as a C struct with free functions -- zero overhead.
- Constructors/destructors: Map naturally to peripheral init/deinit, but watch for hidden costs (global constructors run before
main(), destructors may pull inatexit). - Virtual functions: Each virtual call adds one pointer indirection (~2-4 cycles) and requires a vtable (one per class in ROM) plus a vptr (one per instance in RAM).
- CRTP: The Curiously Recurring Template Pattern resolves polymorphism at compile time, producing the same code as a direct call -- zero runtime cost.
- Composition over inheritance: Prefer "has-a" relationships; deep inheritance trees create tight coupling and make ROM usage hard to predict.
- Embedded C++ coding standards: AUTOSAR C++14 and MISRA C++ restrict virtual functions, exceptions, and RTTI to keep resource usage predictable.
Deep Dive
At a Glance
| Aspect | C struct + function pointers | C++ class (no virtuals) | C++ class (with virtuals) | C++ CRTP |
|---|---|---|---|---|
| Runtime overhead | None | None | vptr per object + vtable per class | None |
| Type safety | Weak (void pointers) | Strong | Strong | Strong |
| Polymorphism | Manual dispatch table | None (static binding) | Runtime (vtable) | Compile-time (templates) |
| ROM cost | Function pointers in table | Same as C | Vtable array in ROM | Code duplication per type |
| RAM cost | Pointer in struct | Same as C | +4-8 bytes vptr per object | Same as C |
| Typical use | HAL layers, Zephyr drivers | Register wrappers, RAII | Sensor/protocol hierarchies | High-perf driver abstractions |
Class Basics: Zero Overhead over C
A C++ class with no virtual functions is nothing more than a C struct with associated functions. The compiler generates identical machine code. The member functions receive an implicit this pointer exactly like a C function receiving a pointer to a struct:
C struct pattern: C++ class equivalent:──────────────────── ────────────────────struct led { class Led {uint32_t *port; uint32_t *port_;uint8_t pin; uint8_t pin_;}; public:void led_on(struct led *l) { void on() {*l->port |= (1U << l->pin); *port_ |= (1U << pin_);} }};
Both produce a load, an OR with a shifted mask, and a store. The C++ version adds no instructions, no vtable, and no extra RAM. This is the foundation of embedded C++: classes are a compile-time organizational tool, not a runtime cost.
Encapsulating Hardware Registers
Wrapping register access in a class enforces correct usage at compile time. Private members prevent application code from doing raw read-modify-write on hardware registers without proper bit masking:
class Gpio {volatile uint32_t *const odr_; // output data registervolatile uint32_t *const idr_; // input data registerconst uint8_t pin_;public:Gpio(uint32_t base, uint8_t pin): odr_{reinterpret_cast<volatile uint32_t *>(base + 0x14)},idr_{reinterpret_cast<volatile uint32_t *>(base + 0x10)},pin_{pin} {}void set() const { *odr_ |= (1U << pin_); }void clear() const { *odr_ &= ~(1U << pin_); }bool read() const { return (*idr_ >> pin_) & 1U; }};
The compiler inlines set(), clear(), and read() at -O2, producing code identical to bare-metal register access. The class adds type safety (cannot accidentally pass a UART base address to a GPIO) and prevents direct register manipulation that bypasses the pin mask.
Methods like set() and read() that operate through volatile pointers should be marked const because they do not modify the object's logical state -- they modify hardware. This lets you use const Gpio& references in APIs that should not reconfigure the pin.
Constructors and Destructors: Init/Deinit
Constructors and destructors map naturally to the peripheral init/deinit pattern that every embedded developer already uses. The constructor configures the peripheral, and the destructor releases it:
Benefits: Resource acquisition is initialization (RAII) -- if the object goes out of scope, the destructor automatically deinits the peripheral. No chance of forgetting to disable a clock or release a DMA channel.
Hidden costs to watch for:
| Concern | Detail |
|---|---|
| Global constructors | Constructors of global/static objects run before main() via .init_array. If your startup code does not call these, globals stay uninitialized. |
| Destructor side effects | Declaring a destructor may cause the linker to pull in atexit() and heap support, increasing ROM usage by hundreds of bytes on some toolchains. |
| Construction order | The C++ standard does not define the order of construction across translation units. Two globals in different .cpp files with dependencies on each other produce undefined behavior. |
| Exceptions in constructors | Many embedded targets disable exceptions (-fno-exceptions). If a constructor cannot fail, this is fine. If it can, you need a two-phase init pattern (init() method that returns an error code). |
On bare-metal targets, the startup file (startup.s or crt0) must iterate .init_array to call global constructors before main(). Vendor-provided startup code for C-only projects sometimes omits this step. If your C++ globals are not initialized, check that .init_array is called.
Inheritance for Device Hierarchies
Inheritance lets you define a base interface and implement it differently for each concrete device. A typical embedded hierarchy:
Sensor (base)/ \TempSensor AccelSensor/ \I2CTempSensor SPITempSensor
The base Sensor class defines the interface (init, read, sleep). Each derived class implements those operations for its specific hardware. Higher-level code operates on Sensor& references without knowing the concrete type.
This is the same pattern as the C vtable (struct of function pointers) described in the function pointers topic, but with compiler-enforced type checking, automatic dispatch, and no manual null-check boilerplate.
When inheritance works well in embedded: one level of depth (base interface + concrete implementations), small number of implementations (2-5 sensor types), and the interface is stable (does not change across firmware versions).
When to avoid inheritance: deep hierarchies (3+ levels) that create fragile coupling, frequent interface changes that cascade through the tree, or tight ROM budgets where vtable overhead matters.
Virtual Functions and Vtable Cost
When a class has at least one virtual function, the compiler generates a vtable -- an array of function pointers stored in ROM. Every instance of that class carries a vptr -- a hidden pointer to its vtable, stored in RAM alongside the object's data members.
Cost breakdown:
| Item | Size | Stored in |
|---|---|---|
| Vtable (one per class) | N x pointer size (4-8 bytes per virtual method) | ROM (.rodata) |
| Vptr (one per instance) | 4 bytes (32-bit) or 8 bytes (64-bit) | RAM (inside object) |
| Virtual call overhead | ~2-4 extra cycles (load vptr, index vtable, indirect call) | CPU |
On a Cortex-M with 32 KB flash and 8 KB RAM, a class hierarchy with 5 virtual methods and 3 classes costs 60 bytes of ROM (3 vtables x 5 entries x 4 bytes) and 4 bytes of RAM per instance. For a single sensor object, 4 bytes is negligible. For 100 objects in an array, that is 400 bytes of RAM -- potentially significant.
The bigger concern on embedded targets is predictability: virtual calls go through an indirect branch, which defeats branch prediction on simple cores and makes worst-case execution time harder to bound. Safety-critical standards (AUTOSAR C++14 Rule A10-0-1, MISRA C++ Rule 10-0-1) restrict or forbid virtual dispatch in time-critical paths for this reason.
CRTP: Zero-Cost Polymorphism
The Curiously Recurring Template Pattern (CRTP) resolves polymorphism at compile time. The base class is a template parameterized on the derived class. It calls derived methods through a static_cast to the derived type -- no vtable, no vptr, no indirect call:
template <typename Derived>class SensorBase {public:int16_t read() {// Compile-time dispatch: calls Derived::read_impl() directlyreturn static_cast<Derived*>(this)->read_impl();}void sleep() {static_cast<Derived*>(this)->sleep_impl();}};class TempSensor : public SensorBase<TempSensor> {public:int16_t read_impl() { /* read I2C temp register */ return 0; }void sleep_impl() { /* set sensor to low-power */ }};class AccelSensor : public SensorBase<AccelSensor> {public:int16_t read_impl() { /* read SPI accel register */ return 0; }void sleep_impl() { /* set accelerometer to standby */ }};
The compiler inlines read_impl() through the static_cast, generating the same code as a direct function call. No vtable in ROM, no vptr in RAM, no indirect branch.
| Feature | Virtual dispatch | CRTP |
|---|---|---|
| Dispatch resolved at | Runtime | Compile time |
| Vtable / vptr overhead | Yes | No |
| Can store mixed types in one container | Yes (Sensor* array) | No (each instantiation is a different type) |
| Code size | One copy of base methods | Base methods duplicated per derived type |
| Branch prediction | Indirect (unpredictable) | Direct (predictable) |
| Best for | Heterogeneous collections, plugin architectures | Performance-critical paths, fixed set of types |
Trade-off: CRTP duplicates the base class code for each derived type (template instantiation), which can increase ROM if there are many derived types with large base methods. Virtual dispatch shares one copy of the base code. Profile before choosing.
When an interviewer asks "how do you avoid vtable overhead in C++?", CRTP is the expected answer. Pair it with the cost numbers: "A vtable costs N x 4 bytes in ROM per class and 4 bytes per instance in RAM. CRTP eliminates both by resolving dispatch at compile time."
Composition vs Inheritance
In desktop C++ the advice "prefer composition over inheritance" is about flexibility. In embedded C++ it is also about resource control.
Composition ("has-a"): A MotorController has a PwmDriver and an Encoder. Each component is a member object. The controller delegates to them.
Inheritance ("is-a"): A BrushlessMotor is a Motor. The base class defines the interface; the derived class implements it.
| Criterion | Composition | Inheritance |
|---|---|---|
| Coupling | Loose -- components are independent | Tight -- derived class depends on base internals |
| ROM predictability | Each component's size is self-contained | Virtual methods add vtables; deep hierarchies cascade |
| Testability | Swap in a mock component easily | Must mock the entire base class |
| Flexibility | Can change components at compile time (template) or runtime (pointer) | Locked into the class hierarchy at compile time |
| When to use | Default choice for combining capabilities | When you need a common interface with substitutable implementations |
For embedded systems, the practical guideline: use inheritance for one-level-deep interface abstraction (base interface + concrete implementations) and composition for everything else.
Debugging Story: The Vtable That Vanished
A team porting a sensor library from an ARM Cortex-A (Linux) to a Cortex-M0 (bare metal) hit a mysterious hard fault during the first sensor->read() call. The crash was at the vptr dereference -- the vptr was 0x00000000.
The root cause: the Cortex-M0 project used a C-only startup file that did not call global constructors (the .init_array section). On the Linux target, crt0 handled this automatically. On bare metal, the Sensor global object's constructor never ran, so the vptr was never initialized -- it sat in .bss as zero.
The fix was adding the .init_array iteration loop to the startup assembly. The broader lesson: C++ on bare metal requires startup support that C projects do not need. If your objects have vtables and you see a null vptr, check that global constructors are being called.
What interviewers want to hear: You understand that non-virtual C++ classes have zero overhead compared to C structs. You can quantify vtable cost (N x 4 bytes ROM per class, 4 bytes RAM per instance) and know it matters most on small MCUs or large object arrays. You know CRTP as the compile-time alternative to virtual dispatch. You default to composition and reserve inheritance for one-level-deep interface abstraction. You are aware of the startup requirements for C++ on bare metal (.init_array, global constructors) and the hidden costs that destructors can introduce (linker pulling in atexit).
Interview Focus
Classic Interview Questions
Q1: "What is the overhead of a C++ class compared to a C struct in embedded?"
Model Answer Starter: "A C++ class with no virtual functions compiles to identical machine code as a C struct with free functions. The member functions receive an implicit this pointer, just like passing a struct pointer in C. There is zero runtime overhead -- no vtable, no extra RAM, no indirect calls. The overhead only appears when you add virtual functions: one vtable per class in ROM and one vptr (4 bytes on 32-bit) per instance in RAM."
Q2: "Explain what a vtable is and quantify its cost on a Cortex-M."
Model Answer Starter: "A vtable is a compiler-generated array of function pointers, one entry per virtual method, stored in ROM. Every object of that class carries a hidden vptr pointing to the vtable. On a 32-bit Cortex-M, each vtable entry costs 4 bytes of ROM, and each vptr costs 4 bytes of RAM per instance. A class with 5 virtual methods and 10 instances costs 20 bytes ROM (one vtable) plus 40 bytes RAM (10 vptrs). The runtime cost is one extra indirection per virtual call -- about 2-4 cycles on Cortex-M."
Q3: "How does CRTP avoid the cost of virtual functions?"
Model Answer Starter: "CRTP makes the base class a template parameterized on the derived class. The base calls derived methods through static_cast<Derived*>(this)->method(), which the compiler resolves at compile time to a direct function call. There is no vtable in ROM and no vptr in RAM. The trade-off is that CRTP types cannot be stored in a single heterogeneous container like virtual types can, and the base class code is duplicated for each template instantiation, which can increase ROM if there are many derived types."
Q4: "When would you use inheritance vs composition in embedded C++?"
Model Answer Starter: "I default to composition -- 'has-a' -- for combining capabilities, because it keeps components loosely coupled and independently testable. I use inheritance -- 'is-a' -- only for one-level-deep interface abstraction: a base interface with substitutable concrete implementations, like a Sensor base with I2CSensor and SPISensor derived classes. I avoid deep inheritance hierarchies because they create tight coupling, make ROM usage hard to predict, and are discouraged by AUTOSAR C++14."
Q5: "What startup requirements does C++ add on a bare-metal target?"
Model Answer Starter: "C++ requires the startup code to iterate the .init_array section to call global constructors before main(). Standard C startup only zeroes .bss and copies .data. If .init_array is not called, global objects with constructors -- including anything with a vtable -- will be uninitialized. Additionally, if any class has a destructor, the linker may pull in atexit() and its dependencies, adding hundreds of bytes of ROM. On many bare-metal projects, we use -fno-exceptions and -fno-rtti to eliminate exception handling and runtime type information overhead."
Trap Alerts
- Don't say: "C++ is too heavy for embedded" -- non-virtual classes, CRTP, and constexpr have zero runtime overhead. The language features that cost resources (exceptions, RTTI, virtual dispatch) can be selectively disabled or avoided.
- Don't forget: The
.init_arraystartup requirement for C++ on bare metal. This is the single most common cause of "C++ works on Linux, crashes on the MCU" bugs. - Don't ignore: Code size implications of templates and CRTP. Each template instantiation duplicates the base class code. On a 32 KB flash target with 10 sensor types, this can matter more than vtable overhead.
Follow-up Questions
- "How do
-fno-exceptionsand-fno-rttireduce code size on embedded targets?" - "Can you have a virtual destructor in embedded C++, and what does it cost?"
- "How does Zephyr RTOS use C-style vtables instead of C++ virtual functions?"
- "What is the
overridekeyword and why does it prevent bugs?" - "How would you enforce at compile time that a derived CRTP class implements all required methods?"
- "What happens if you create an array of CRTP base pointers?"
For more C/C++ interview questions with collapsible answers, see the C/C++ Embedded Interview Questions page.
Practice
❓ What is the runtime overhead of a C++ class with no virtual functions compared to a C struct?
❓ On a 32-bit Cortex-M, a class with 6 virtual methods costs how many bytes of ROM for the vtable?
❓ How does CRTP achieve polymorphism without a vtable?
❓ What happens on bare metal if the startup code does not call the .init_array section?
❓ Why do embedded C++ coding standards (AUTOSAR, MISRA C++) restrict virtual functions?
Real-World Tie-In
Automotive Sensor Fusion ECU -- A Cortex-M7 sensor fusion module uses CRTP-based sensor drivers for radar, lidar, and camera inputs. Each sensor type instantiates the same SensorBase<T> template, and the compiler inlines all dispatch. The team measured zero overhead vs hand-written C dispatch tables, while gaining compile-time type checking that caught two interface mismatches during a sensor swap -- errors that would have been silent cast-away bugs in C.
IoT Gateway with Plugin Drivers -- An IoT gateway supporting Modbus, BACnet, and MQTT protocol adapters uses a single-level virtual hierarchy: a ProtocolAdapter base with three concrete implementations. The vtable costs are acceptable (3 vtables x 4 methods x 4 bytes = 48 bytes ROM, plus 4 bytes per adapter instance in RAM) because there are only three adapter instances. The runtime polymorphism lets the gateway route messages without knowing the protocol at compile time.
Medical Wearable Power Management -- A medical wearable's power manager uses composition exclusively: a PowerManager class has a BatteryMonitor, a ChargingController, and a SleepScheduler, each as member objects. The team initially tried a three-level inheritance hierarchy but abandoned it when a base class change broke all derived classes during a silicon revision. Composition let them swap the ChargingController implementation for the new charger IC without touching the rest of the system.
