Blog

Modern C++ software architecture

The road from “we do embedded C++” to “we understand embedded C++”: what we learned about modular architecture, stumbling blocks included.

A whitepaper on reusable modules, by Florian Seibold

For some years now we have been travelling the road from “we do embedded C++” to “we understand embedded C++”. We regularly come across stumbling blocks along the way, and so far they have gradually cleared themselves up. In this article I would like to share what we have learned and get you interested in modular architecture design and thinking in modules.

When we overhauled our approach to embedded software development in 2017, there was no library yet for the bread-and-butter work of embedded development. So we had to build almost everything ourselves.

Since then we ask ourselves the following questions before we implement a task:

  • Does the standard library offer something that solves most of my task? If so, the problem has usually taken care of itself.
  • Is my approach generic enough that others can put my implementation to good use, so that the effort pays off?
  • Is my approach efficient in footprint and CPU load for every use case the module is described for?

Why do we ask ourselves these questions?

Over the past years, different trends kept emerging internally that needed keeping in check. There was a time, for instance, when the implementer pattern was part of every second solution. With these three easy questions we try to create modules that make sense: they do not reimplement the standard library, they can be reused elsewhere, and they are as efficient and as suitable for embedded use as possible. All of that first turned our way of working upside down and then put it in order again. The quality, robustness and clarity of our results have risen noticeably, and our clients appreciate it too.

Architecture is something people argue about, and rightly so. I believe that every constructive discussion, however heated it may get at times, sooner or later makes source code noticeably more professional.

Whether an architecture is clear and built around its users only shows when people work with it who were not involved in developing it. We regularly ask our clients for feedback: how far do they understand what happens in the code, and where would they expect to find which functionality? We go one step further with new colleagues, who still see things from the outside and can help us resolve structural problems we no longer notice ourselves. Step by step we have arrived at this interim result: architecture that keeps working for our clients and for us is always built the same way in the base layers, and is therefore generic.

Our aim is to produce small, comprehensible software modules that add clear value across different projects and use cases, and that developers are happy to reuse.

So much for the theory. In practice it takes a philosophy and clear rules to develop by. For me it has become clear that every module must meet four criteria:

  • Functional: software is functional when it does exactly the job it is described for, and does it effectively. Extra frills that add no clear value for the module and its users have no place in it.
  • Reusable: software is reusable when it is generic enough that a whole category of matching use cases can put it to good use.
  • Testable: unit, integration and system tests have become indispensable in embedded software too. Whether the issue is liability for safety functions or fewer customer complaints, well-made unit tests can achieve a great deal. But unit tests are only as good as the structure of the class they test. Only functions of low complexity can be tested reliably and traceably.
  • User-friendly: my favourite criterion, and probably the hardest of the four. Using the module has to be enjoyable. The API, the documentation and the examples have to be so easy to understand that outside developers would rather use the module than work out an implementation of their own.

The Average class serves as a small example here. Its two static functions calculate an arithmetically correct mean from the data array passed to them, or from the contents of a buffer object, which may be a ring buffer, for instance, and return it. In this example, Buffer is an interface for all buffer implementations. The arithmetic in the two value() functions is optimised for embedded use.

I think it is clear what functionality to expect from the class and how to use it. What you cannot see now, but probably suspect, I can confirm: the class is easy to test and easy to reuse.

The full report with code examples, explanations and lessons learned is available here as a PDF:

Download PDF: Modern C++ software architecture

Wishing you plenty of good lateral thinking,

querdenker engineering