Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 41

Inheritance vs. Composition in Embedded C++

Recommended resources

Hello! Welcome to the first lesson of Module 6: Modern Embedded C++.

In the previous module, we sharpened our skills with C-based algorithms, culminating in a deep dive into Heapsort and its critical O(1) space complexity advantage in memory-constrained systems. Now, we shift gears to explore how the abstractions in C++ can be leveraged to write more maintainable and modular embedded software, without compromising on performance or predictability.

This lesson tackles one of the most fundamental design choices in object-oriented programming: the relationship between inheritance and composition. While you have likely used both, the focus here is on developing the architectural judgment required in senior engineering roles and interviews—knowing not just how to use them, but why and when one is superior to the other, especially in an embedded context.

Your learning outcome is to: Apply inheritance and composition in C++, justifying when to prefer composition for flexible and maintainable embedded designs.


1. A Common Embedded Problem: Managing Hardware Variants

Imagine you are writing firmware that needs to support multiple hardware revisions or similar products. A classic example is blinking an LED, where one board uses a simple GPIO and another uses a more complex driver for an RGB LED like a DotStar.

A common, but brittle, approach in C is to litter the code with preprocessor directives (#ifdef, #else, #endif) to switch between implementations. While functional for small cases, this quickly becomes unreadable and unmaintainable as the number of variants and features grows.

C++ offers a much cleaner solution through polymorphism. Let's start by seeing how inheritance provides an initial, elegant-seeming answer to this problem.

Arduino & C++: Blink Sketch Reinvented with Inheritance & Composition

This video from atomic14, 'Arduino & C++: Blink Sketch Reinvented with Inheritance & Composition', provides an excellent, practical walkthrough. We'll start by watching the sections that set up the problem and solve it using inheritance.

Please watch from 00:32 to 06:16. Pay attention to: The initial, hard-to-maintain #ifdef based code. The C++ recap of abstract base classes and pure virtual functions (e.g., the Animal class). How an abstract Led base class with on() and off() methods is created. How concrete classes (GenericLed, TinyPicoLed) inherit from the base class to provide specific hardware implementations.

As the video demonstrates, this "is-a" relationship (a TinyPicoLed is-a type of Led) allows the main application logic to operate on a base Led* pointer, completely decoupled from the underlying hardware specifics. This is a powerful abstraction.

A frequent concern in embedded C++ is code size bloat. Does this pattern of having multiple derived classes unnecessarily increase firmware size? The video addresses this directly.

Arduino & C++: Blink Sketch Reinvented with Inheritance & Composition

Let's continue with the same video to quickly address the code size concern.

Watch from 06:16 to 07:13. The key takeaway is that the linker is smart enough to perform dead-code elimination. Unused concrete classes are not linked into the final binary, so you only pay the memory cost for the implementations you actually instantiate.


2. The Limits of Inheritance: When "Is-A" Becomes a Straitjacket

Inheritance works well for the simple LED example. However, as system complexity grows, relying heavily on inheritance can lead to rigid, tightly coupled designs. This is because inheritance creates one of the strongest forms of coupling in object-oriented design. A change in the base class can have a ripple effect, forcing changes in all derived classes.

Let's explore a scenario where a pure inheritance model starts to fail.

Arduino & C++: Blink Sketch Reinvented with Inheritance & Composition

The final segment of the video illustrates how a deep inheritance hierarchy becomes problematic when you need to mix and match different functionalities (e.g., different temperature sensors, ADCs, and buzzers).

Watch from 07:13 to 09:14. Notice how trying to reuse code by inheriting from an existing class with a specific buzzer implementation leads to a messy, inflexible hierarchy. The video then introduces the solution: composition.

This leads us to a core principle of modern object-oriented design: "Prefer composition over inheritance."

  • Inheritance (is-a): A TemperatureSensorDriver is-a SensorDriver. This is static and defined at compile-time.
  • Composition (has-a): A SensorDriver has-a SensorComponent. This relationship is often more dynamic and flexible.
Inheritance vs. Composition in Sensor Driver Design
This UML diagram shows two design approaches for sensor drivers. On the left, an inheritance model creates a rigid hierarchy. On the right, a composition model allows a generic `SensorDriver` to be composed with different `SensorComponent` objects, offering greater flexibility and better performance.

Why "Inheritance Breaks Encapsulation"

To formalize this for an interview setting, you need to articulate why composition is often better. The key reason is that inheritance can break encapsulation.

Design Patterns (C++): Introduction

The following article from bogotobogo.com, 'Design Patterns (C++): Introduction', provides the theoretical backbone for this discussion. It clearly defines the terms and explains the pitfalls of inheritance.

Please read the sections titled "Disadvantages of Inheritance" and "How about the composition". Focus on understanding the C++ example that shows how changing a method signature in the base Animal class forces changes in the main() function, even though main() only uses the derived Cat class. Then, see how the composition-based approach isolates this change.

In summary, because a derived class is so tightly bound to its parent's implementation, it's not a true "black box." Changes to the parent's protected or public members can force modifications in the child, breaking the child's encapsulation. Composition, by contrast, uses a well-defined public interface (the "back-end" class), creating a much looser coupling and better insulation from changes.


3. An Interview-Ready Pattern: Polymorphism Without the Heap

A major reason embedded developers have historically been wary of C++ polymorphism is its typical reliance on dynamic memory allocation (new) to create objects on the heap. As you know, heap usage in many embedded systems is undesirable due to non-determinism, fragmentation, and the risk of allocation failures.

So, how can we get the flexibility of polymorphism (like in the Led example) without the heap? The answer is a powerful pattern combining composition and placement new.

How to Have Polymorphism Without a Heap

This Barr Group article, 'How to Have Polymorphism Without a Heap', is a goldmine for embedded C++ interviews. It presents a concrete, practical solution to this exact problem.

Please read the entire article, but we will analyze it in two parts. First, read the sections "Heap Problems in Embedded Systems" and the first half of "Polymorphism without a Heap" (down to Listing 2). This presents an initial, inheritance-based solution using placement new inside a fixed-size buffer. Then, read the section "Another Approach to Non-Heap Polymorphism". This presents the improved, composition-based design. This is the key pattern to understand.

Let's break down the superior pattern from the end of the article:

  1. The Goal: We want a Configuration object that can hold different types of Instrument objects (Piano, Violin), which are selected at runtime.
  2. The Problem: We cannot use new Piano() because that uses the heap.
  3. The Solution:
    • An abstract base class Instrument defines the interface (e.g., virtual void playNote()).
    • A "holder" class, GenericInstrument, is created. It does not inherit from Instrument. Instead, it contains a char buffer large enough to hold any concrete Instrument object. This is composition.
    • GenericInstrument cleverly overloads the operator->() to return a properly cast Instrument* pointer to the start of its internal buffer.
    • To create a specific instrument, we use placement new to construct, for example, a Piano object directly inside the GenericInstrument's buffer: new(c1.i.get()) Piano;
    • To use the instrument, we can now write intuitive code: c1.i->playNote('c');. The overloaded -> handles the pointer conversion, and the virtual function call dispatches to the correct Piano::playNote implementation.

This technique gives you the full power of runtime polymorphism with:

  • No heap allocation: Memory is statically allocated within the Configuration object.
  • Clean syntax: operator-> makes usage seamless.
  • Strong encapsulation: The Configuration object fully owns and manages the lifetime of its Instrument.

This is an excellent, advanced pattern to have in your toolkit for a system design interview question.


4. Advanced Perspective: Zero-Overhead Composition

For the most demanding applications, even the overhead of a virtual function call (which we will dissect in the next lesson) might be too much. Modern C++ offers ways to achieve compositional flexibility at compile-time, resulting in zero runtime overhead.

This is an advanced topic that can be a real differentiator in an interview, showing you are on the cutting edge of embedded C++ practices.

Composition on Tiny Embedded Systems in C++ - Luke Valenty - CppNow 2023

This CppNow talk by Luke Valenty, 'Composition on Tiny Embedded Systems in C++', demonstrates how to build highly modular firmware for severely constrained microcontrollers (1KB of flash, 64 bytes of RAM) using compile-time composition.

You don't need to understand every detail of the custom library being used. Focus on the high-level concepts. Watch 24:15 - 27:19: Understand the idea of 'services' (extension points) and 'features' (pluggable modules). The key is that features depend on the core, not the other way around. This is dependency inversion. Watch 51:47 - 54:19: See how an IR light controller is integrated. The IR logic (IR light controller) doesn't know about the specific LED; it talks to a generic light controller. The components are composed together by a configuration file, ensuring dependencies flow in the right direction. This creates a highly modular system.

The core idea from this talk is that by describing the components and their dependencies to the build system, you can have the compiler assemble the final function calls directly. This effectively "inlines" the entire logic, removing the need for runtime mechanisms like v-tables or function pointers while still allowing you to develop with small, independent, and composable modules.



Conclusion

In this lesson, we moved beyond the simple syntax of class A : public B to a deeper, architectural understanding of code reuse. You learned not only how to apply inheritance and composition but also how to justify your design choices—a critical skill for a senior engineer.

Key Takeaways:

  • Inheritance ("is-a") is a powerful tool for polymorphism but creates tight coupling, which can make systems rigid and hard to maintain. It can "break encapsulation."
  • Composition ("has-a") is generally preferred for its flexibility and stronger encapsulation. It allows you to build complex objects from smaller, independent parts, leading to more modular and testable code.
  • For embedded systems, you can achieve safe, heap-free polymorphism by composing a class with a buffer and using placement new to construct polymorphic objects within it.
  • Advanced C++ techniques even allow for compile-time composition, providing zero-overhead abstractions for the most constrained environments.

Preview of the Next Lesson:

We've discussed virtual functions as the mechanism that enables runtime polymorphism in inheritance-based designs. But how do they actually work? In the next lesson, we will get to the heart of the matter: Explain V-Table layout and the virtual function dispatch mechanism, and quantify its memory and performance overhead. This will give you the knowledge to make informed decisions about when the cost of virtual is justified.