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;
isoint i = 1;
- adding ticks to numbers, e.g.
1'000'000
iso1000000
, and0x1002'0030
iso0x10020030
- 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 usefun (int)
, with a space in between - likewise, array indexing uses
a[123]
, array definition is written asint a [123]
- opening braces are at the end of
if
,while
, etc (to avoid lines with no real content)