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 meet stumbling blocks along the way, and so far each of them has been cleared. In this article I would like to share what we have learned and to make you curious about modular architecture and thinking in modules.

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

Since then we ask ourselves the following questions before implementing anything:

  • 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 years, various internal trends kept emerging that needed keeping in check. There was a time, for instance, when the implementer pattern was part of every second solution. With these few easy questions we try to create modules that make sense. Sense, because they do not reimplement the standard library, because they can be reused elsewhere, and because they are built to be efficient and suited to embedded work. All of that first turned our way of working upside down and then put it in order again. The quality, robustness and clarity of what we produce have risen noticeably. Our clients thank us for it too.

Architecture is something people argue about, and rightly so. I believe that every constructive discussion, heated though it may sometimes be, sooner or later leads to a jump in the professionalism of the source code.

Whether an architecture is also clear and oriented towards its users only shows when people work with it who were not involved in building it. In our case we regularly gather feedback from our clients and ask how far they understand what happens in the code and where they would expect to find which functionality. We go one step deeper with new colleagues, who still have an outside view and can help us resolve structural problems we no longer see ourselves. Iteratively 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 a developer is happy to reuse.

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

  • Functional: software is functional when it does effectively and precisely the job it is described for. Additional frills that add no clear value for the module and its users have no place in it.
  • Reusable: software is reusable when it is built generically enough to serve a whole matching category of use cases.
  • Testable: unit, integration and system tests are no longer dispensable in embedded software either. Whether the subject is liability for safety functions or a reduction in complaints, well-made unit tests can achieve a great deal. And unit tests are only as good as the structure of the class they are meant to test. Only functions of low complexity can be tested stably and comprehensibly.
  • Usable: 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 clear enough that people outside would rather use the module than think up an implementation of their own.

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

I think it is clear what functionality one can expect from the class and how it is used. What you cannot see but will 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 as a PDF:

Download PDF: Modern C++ software architecture

Wishing you plenty of lateral thinking, your querdenker engineering.