Skip to content

C++ and coding style

As of 2021, I’ve adopted some new programming language features and a certain code style. This page will give an overview and some background.

Language

While there are alternatives, the main programming language for embedded µC software development is still C and its derivative, C++.

C++17

C++ is a complex language with a fairly steep learning curve. In the past decade, it has become even more advanced, with two major leaps being C++11 and C++17 (the numbers are roughly the year of introduction, i.e. 2011 and 2017). There’s a 3-year cycle, with C++23 being worked on now, but these are current major revisions with the most important changes.

I’ll be using C++17, since it’s stable and available for all mainstream µCs, as well as on the “native” hosts I’m working on, i.e. MacOS and Linux.

C++ has supported virtual methods and templates for a long time now. C++11 adds lambdas and the auto keyword, to name a few key changes. C++17 adds good support for constexpr, which lets the compiler do a lot more work at compile time.

When given free reign, today’s compilers can generate incredibly efficient and compact code. This does require placing a lot of the source code in headers, however.

One clear trend in C++ compared to “plain” C, is that #define and #if are being phased out, i.e. the language-agnostic macro pre-processor is gradually being made less important.

No STL

There is a “Standard Template Library” which includes a wide range of data structures and algorithms. While extremely powerful, not all of this code travels well to the resource-constrained world of microcontrollers.

Since my mantra is “less, please”, I will not be using the STL in my code. I’m not against it, I just don’t think it’ll help me that much.

References

The main stumbling point when reading C++ code when you’re used to C, is the use of the & operator in an unexpected way. In C, &a means “the address of a”. It’s the same in C++, but there’s also int& b, which makes no sense in C.

The way to read this is as a “hidden” pointer: the b name looks like a value, but it’s actually a pointer. This means you can write b = ... to mean: store a value in the thing b refers to.

This notation is pervasive in C++. It wasn’t added on a whim, it’s in fact essential for some of the features added to C++.

I use references everywhere. I also use it to pass what is essentially a pointer, but with the implied convention that it’s never the null pointer.

Operator overloading

There are some really nice notations you can achieve in C++ by using operator overloading and a few more tricks. This is what makes it possible to write if (rfDev(0x10)) {...}, even though rfDev(0x10) might refer to a register inside a connected SPI device, for example.

This is also how led = 1 and if (button) {...} can look like variable accesses in C++, even though underneath there is real hardware involved.

I tend to often play tricks with operators and overloading. The main goal here is always to end up with a more natural coding style in actual use, than precedural notations can provide.

Virtual methods

I try to use virtual methods sparingly. They can be very convenient, but they are a run-time mechanism.

When implementing an LCD driver library, for example, you want it to be as widely usable and generic as possible. This doesn’t mean that the driver will be switched at run time, however! There will hardly ever be a need to support two different LCD displays in the same application. And while flexibility is key for a library, this does not mean that the library should be figuring out everything at run time. So when drawing on the screen, it really doesn’t need to use a dynamic function dispatch each time it is drawing a pixel.

Templates address this problem, even though they used to be fiendishly difficult to use in early C++ releases (with lengthy and cryptic syntax error messages when mis-used). The point is that a compiler can tell at compile time how the code is going to fit together, and then optimise pixel drawing for the display that’s being used in that particular context.

In short: yes, I’ll definitely use virtual methods. But in moderation.

Templates

C++ templates have a bad name, probably mostly for the reason described above. In a way, templates are the type-safe successor to C’s #define macros. They let you write code without having all the underlying code available. They are literally “templates” which the compiler can use to write the real thing. That means they can be “morphed” (as I call it) into the definitive implementation when “instantiated”.

Templates are not for the faint of heart - writing template code is quite a complex task. But using template code is often quite simple, and at times they will lead to a very clean and intuitive way of fitting different pieces together.

I’ll be using C++ templates, mostly in simple ways since I haven’t fully grasped the details of things like variadic templates and meta-programming (and don’t see a need for them in µC context, to be honest). And again: I’ll be using templates in moderation.

Constexpr

The constexpr keyword was introduced in C++11, but C++14 and C++17 take it to new heights. It’s a way to tell the compiler that - where it can - it should try to perform most of the computations at compile-time.

To give an example: strlen("abc") will computer as the constant 3. You could in fact use it to dimension an array, if you wanted to.

The constexpr mechanism really makes a difference w.r.t. defining objects which are … constant, and which can be pre-initialised by the compiler. This can substantially reduce the amount of generated initialisation code. Which is always nice in the resource-constrained embedded world, especially since such code is likely to only run once anyway, on power-up.

I try to use constexpr where I can. It sometimes leads to amazing code reductions.

Namespaces

Namespaces are a great way to keep names from clashing with each other. It’s fine to call a function open(), if it’s in a namespace device, for example, since that means it can be used as device::open() from outside the namespace, or simply as open() while inside the namespace context.

I tend to define a few namespaces with short, lower-case names.

Conveniences

And then there are the many little conveniences that C++17 has to offer, such as:

  • using the auto keyword to derive types, e.g. auto i = 1; iso int i = 1;
  • adding ticks to numbers, e.g. 1'000'000 iso 1000000, and 0x1002'0030 iso 0x10020030
  • for loops over containers, e.g. for (auto e : myArray)
  • lambdas for simple call functions, e.g. auto x = [](int i) { return i*i; };
  • lambda closures for simple functions inside other functions
  • the auto keyword for function results, e.g. auto f (int) -> bool;
  • using the built-in nullptr instead of the “NULL” defined as zero in C

Coding style

Style is a matter of taste. My coding style is terse. I choose i as index variable, when the context is only a few lines of code. I combine expressions using a ? b : c when I can, instead of if / else. I don’t add squiggly braces around single statements. And so on …

My intention is to be able to grasp a screen of source code in my head. And to that end, I want to fit things on a screen, with limited amounts of white space. More controversial perhaps, is that I don’t want to skip over lots of comments (even with a “folding” editor). Brief comments, therefore, and only for what isn’t obvious. Everything else goes into (separate!) documentation.

Notation

Some conventions are simply that - ways of trying to stay consistent across an entire code base, to help more quickly understand what the code means:

  • variable names use lowerCamelCase, as ar functions and methods
  • function calls use fun(arg), function definitions use fun (int), with a space in between
  • likewise, array indexing uses a[123], array definition is written as int a [123]
  • opening braces are at the end of if, while, etc (to avoid lines with no real content)