> There are multiple articles of how C++ is superior to C, that everything you can do in C you can do in C++ with a lot of extras, and that it should be used even with bare metal development
An interesting perspective. Could turn it around as "everything you can do in C++ you can do in C with a lot less language complexity".
My personal experience with low-level embedded code is that C++ is rarely all that helpful, tends to bait you into abstractions that don't really help, brings additional linker/compiler/toolchain complexity and often needs significant extra work because you can't really leverage it without building C++ abstractions over provided C-apis/register definitions.
You definitely need discipline to use C++ in embedded. There are exactly 2 features that come to mind, which makes it worth it for me: 1) replacing complex macros or duplicated code with simple templates, and 2) RAII for critical sections or other kinds of locks.
Consteval is great for generating lookup tables without external code generators. You can use floating point freely, cast the result to integers, and then not link any soft float code into the final binary.
> Could turn it around as "everything you can do in C++ you can do in C with a lot less language complexity".
No, you can't, C is lacking a lot that C++ brings to the table. C++ has abstraction capabilities with generic programming and, dare I say it, OO that C has no substitute for. C++ has compile-time computation facilities that C has no substitute for.
Is there an example of the generic programming that you've found useful?
The extent of my experience has been being able to replace functions like convert_uint32_to_float and convert_uint32_to_int32 by using templates to something like convert_uint32<float>(input_value), and I didn't feel like I really got much value out of that.
My team has also been using CRTP for static polymorphism, but I also feel like I haven't gotten much more value out of having e.g. a Thread base class and a derived class from that that implements a task function versus just writing a task function and passing it xTaskCreate (FreeRTOS) or tx_thread_create (ThreadX).
Typed compile-time computation is nice, though, good point. constexpr and such versus untyped #define macros.
My point is trivially true as far as computability goes, but that is not what I ment.
All those abstraction capabilities can be a big detriment to any project, because they always come with a cost, and runtime is far from the only concern.
Specifically in an embedded project, toolchain complications and memory use (both RAM and code) are potentially much bigger concerns than for Desktop applications, and your selection of programmers is more limited as well; might be much more feasible to lock your developers onto acceptable C coding standards than to make e.g. "template metaprogramming" a necessary prerequisite for your codebase and then having to teach your applicants electrical engineering.
Both object oriented programming and compile time computation is doable for a C codebase, just needs more boilerplate and maybe a code-generator step in your build, respectively. But that might well be an advantage, discouraging frivolous use of complexity that you don't actually need, and that introduces hidden costs (understanding, ease of change, compile time) elsewhere.
I find that encapsulation of devices (UART, I2C, etc.) as classes in C++ rather than global functions that take structs, etc. as input arguments in C to be much more manageable. Same for device drivers for individual sensor ICs.
Maybe, but I'd argue that this is a matter of taste, and what does it actually do for you?
In practice: Is it worth wrapping all your vendor-provided hardware abstraction in classes that you write yourself? Designing something like that to be "complete" (i.e. allow full access to all hardware functionality like IRQ/DMA setup) and to still stay hardware-independent is often borderline impossible anyway, and any attempt is invariably going to make your codebase more difficult to understand.
I do. But talking about low-level embedded stuff here.
Generally, the more you deviate from your vendors "happy path", the more busy work/unexpected difficulties you will run into, and a solid grasp of how exactly architecture and toolchain work might become necessary (while staying on the "happy path" allows you to stay blissfully unaware).
With anything low level you should choose the language you’re most comfortable and competent in imo. Learning both the platform and the language is just asking for headaches, control what you can
I would honestly extend this sentiment out to all code. The benefits C++ has over C are much better served by Rust or Go or Python or Lisp, and if "Simple" is what you want, then C is a much better choice.
Honestly, I can't think of a single job for which C++ is the best tool.
I took a brief skim through so apologies if I missed that it was mentioned, but wanted to bring up the Embedded Template Library[0]. The (over)simplified concept is: it provides a statically-allocated subset (but large subset) of the C++ standard library for use in embedded systems. I used it recently in a C++ embedded project for statically-allocated container/list types, and for parsing strings, and the experience was nice.
So I use C++ heavily in the kernel. But couldn't you just set your own allocator and a couple other things and achieve the same effect and use the actual C++ STL? In kernel land, at the risk of simplifying, you just implement allocators and deallocators and it "just works", even on c++ 26.
While tag dispatching used to be a widely used idiom in C++ development, it was a workaround for which nowadays there are much better alternatives with constexpr, and concepts.
Surely one of the obvious reasons you'd want tagged dispatch in C++ isn't obviated by either of those features? Or am I missing something?
Suppose Doodads can be constructed from a Foozle either with the Foozle Resigned or with the Foozle Submitted. Using tagged dispatch we make Resigned and Submitted types and the Doodad has two specialised constructors for the two types even though substantively we pass only the Foozle in both cases.
In a language like Rust all the constructors have names, it's obvious what Vec::with_capacity does while you will still see C++ programmers who thought constructing a std::vector with a single integer parameter does the same because it's just a constructor and you'd need to memorize what happens.
I wouldn't call the idiom you describe (like with unique_lock's defer_lock_t constructor) "tag dispatch"; to me, one defining characteristic of the "tag dispatch idiom" is that the tag you're dispatching on is computed somehow (e.g. by evaluating iterator_traits<T>::iterator_category()). The idiom you're describing, I'd call simply "a constructor overload set" that happens to use the names of "disambiguation tags" to distinguish semantically different constructors because — as you point out — C++ doesn't permit us to give distinct names to the constructor functions themselves.
You use if constexpr with requires expressions, to do poor man's reflection on where to dispatch, and eventually with C++26, you do it with proper reflection support.
While this is helpful in a way this blog also contains outdated information. Given that in embedded you most of the time build the world and have most of your stuff as source code you should never get stuck on an old compiler unless it is a one-shot project.
And that leads to the ability to use modern C++ methods in places where it is highly useful such as the ability to use "if constexpr" and templates to make your somewhat generic code optimal for a specific usage scenario like writing a templated driver that adapts to the specific hardware variant you are building. This means you can often run without making runtime decisions on hardware situations that cannot ever occur because the specific chip or board does not even have the option.
Also I noticed the virtual interface mentioned and I would express a minor point of caution there for embedded developers. A virtual interface is really meant for dynamic/runtime situations where the object behind the interface can change at runtime. In embedded that is a more rare case. Consider using concepts to define such interfaces and pass the concrete objects as template parameters where it makes sense. I have on several occasions these last four years reduced the binary size overhead of products by doing this as an object accessed through a virtual interface prevents the removal of unused methods from the final binary (they have to be present even with devirtualization happening to reduce the indirection cost).
Also template bloat is real but also a somewhat over-"hyped" problem in embedded. It occurs when the same template is run several times to produce many variations of the same template. In most template programs this will not happen by accident. Mainly avoid making any reoccuring template configuration apart of the type. Instead we now have the option of initializing objects during compile time using constexpr objects. Make your configuration of the object that way (maybe even consteval) and use that in "if constexpr" if possible. All compilers relevant in embedded can manage to put this configuration into the read-only/flash memory and load it in during start-up/boot.
> Although there is an opinion that templates are dangerous because of executable code bloating, I think that templates are developer’s friends, but the one must know the dangers and know how to use templates effectively. But again, it requires time and effort to get to know how to do it right.
idk man, obviously I don't know much since I don't have my own online book, but templates would not be at the start of my list when selling C++ for bare-metal.
unit suffixes, namespaces, RAII, constexpr, OOP (interfaces mostly), and I like a lot of the STL in avoiding inscrutable "raw loops".
I like the idea of templates, but it feels like a different and specialized skillset. If you are considering C++ from C, why not ease into it?
Most of the good parts of the STL are implemented as templates. If you are considering C++ from C, then treating it like C with the STL templates is a great first step. Over time you will discover other useful features of C++ that solve specific problems. Virtual functions in a class - better than writing a table of function pointers. Lambda - sometimes much better than a function pointer. Custom templates - better than doing the same thing in macros (at least some times). Exceptions are sometimes really nice for error handling (and contrary to popular belief are not slow) And so on - C++ has a lot of features that can be useful, but most of them are features that can also be abused in places they don't belong creating a real problem.
I only skimmed the book, but I think this is an artifact of the embedded engineering side. (Something I do professionally.)
I've seen a lot of new people come into my team as juniors, or regular C/C++ engineers that convert to embedded systems. There is a real lack of good, concise resources for them, and the best result I've had is just mentoring them and teaching as we go.
You could look for an intro to embedded systems resource. Or just get a dev kit for something. Go different than the standard Pi or Arduino. Try and get something like a STM32G0 dev kit working and blinking its lights. It's less polished, but you'll have to touch more things and learn more.
If you want, core areas I would suggest to research are the very low level operations of a processor:
* How does the stack pointer work? (What happens to this during function calls?
* How do parameters get passed to functions by value, by reference? What happens when you pass a C++ class to a function by value? What is a deep vs shallow copy of a C++ object, and how does that work when you don't have an OS or MMU?
* Where is heap memory stored? Why do we have a heap and a stack? How do these work in the absence of an OS?
* The Program Counter? (PC register). What happens to this as program execution progresses?
* What happens when a processor boots, how does it start receiving instructions? (This is vague, you could read the datasheet for something like the STM32G0 microcontroller, or the general Arm Cortex M0 core.)
* How are data/instructions loaded from disk to memory to cache to register? What are these divisions and why do we have them?
* Basic assembly language, you should know how loads and stores work, basic arithmetic, checks/tests/comparisons, jump operations.
* How do interrupts work, what's an ISR, IRQ, IVT? How do peripherals like UART, I2C (also what are these?), handle incoming data when you have a main execution thread already running on a single core processor?
Some of this may be stuff you already know, or seem rudimentary and not exactly relevant, but they are things that have to be rock solid before you start thinking about how compilers for differently languages, like C++, create machine code that runs on a processor without an OS.
Assembly is often overlooked, but a critical skill here. It's really not that bad. Often when working with C++ (or Rust) on embedded systems, if I'm unsure of something, my first step is to decompile and dump the assembly to investigate, or step through the assembly with GDB via a JTAG probe on the target processor if the target was too small to hold all the debug symbols (very common).
Anyways, this may have been more than you were asking for. Just me typing out thoughts over my afternoon coffee.
I'm not a professional embedded engineer, but I do hack around it. Some books I collected:
- Applied Embedded Electronics: Design Essentials for Robust Systems by J. Twomey. It goes over the whole process making a device and what knowledge would be required for each. Making Embedded Systems, 2nd Edition by E. White is a nice complement.
- Embedded System Interfacing by M. Wolf describes the signals and protocols behind everything. It's not necessary as a book, but can help understand the datasheets and standards
- But you want to start with something like Computer Architecture by C. Fox or Write Great Code - Volume 1 - Understanding the Machine, 2nd Edition by R. Hyde. There are better textbooks out there, but those are nice introductions to the binary world.
The gist is that you don't have a lot of memory and CPU power (If you do, adapting Linux is a more practical option unless it's not suited for another reason). So all the nice abstractions provided by an OS is a no go, so you need to take care of the hardware and those are really finicky,
I just thought I'd mention that Khalil Estell is doing some work regarding exceptions in embedded and bare metal programming. In the first of his talks about this topic (https://www.youtube.com/watch?v=bY2FlayomlE) he mentions that a lot of the bloat in exception handling is including printf in order to print out the message when terminating the program.
For those interested he has another presentation on this topic https://www.youtube.com/watch?v=wNPfs8aQ4oo
Didn’t watch the video but as opposed to what? Printf is definitely the most efficient way to retrieve the info needed to begin initial triage especially in “hacking” or bring up where a formal process isn’t defined
There are processors and platforms where including the standard library feature to print text to standard output significantly increases the size of the binary. In such cases just enabling exceptions also enables this feature only for the purpose of outputting a termination message, which does not get displayed or read because the device doesn't have a way to emit standard output.
Using C++ iterator interface to fix the main problem of a standard ring buffer of non-contiguous regions is a cute idea, but I like to use a "bip buffer"[1] instead which actually always gives you real contiguous regions so you can pass the pointers to things like a dma engine.
The tradeoff is that you have in the worse case only half the buffer available - the ring buffer essentially becomes a kind of double buffer where you periodically switch between writing/reading at the end or the beginning of the storage.
An interesting perspective. Could turn it around as "everything you can do in C++ you can do in C with a lot less language complexity".
My personal experience with low-level embedded code is that C++ is rarely all that helpful, tends to bait you into abstractions that don't really help, brings additional linker/compiler/toolchain complexity and often needs significant extra work because you can't really leverage it without building C++ abstractions over provided C-apis/register definitions.
Would not generally recommend.
No, you can't, C is lacking a lot that C++ brings to the table. C++ has abstraction capabilities with generic programming and, dare I say it, OO that C has no substitute for. C++ has compile-time computation facilities that C has no substitute for.
The extent of my experience has been being able to replace functions like convert_uint32_to_float and convert_uint32_to_int32 by using templates to something like convert_uint32<float>(input_value), and I didn't feel like I really got much value out of that.
My team has also been using CRTP for static polymorphism, but I also feel like I haven't gotten much more value out of having e.g. a Thread base class and a derived class from that that implements a task function versus just writing a task function and passing it xTaskCreate (FreeRTOS) or tx_thread_create (ThreadX).
Typed compile-time computation is nice, though, good point. constexpr and such versus untyped #define macros.
All those abstraction capabilities can be a big detriment to any project, because they always come with a cost, and runtime is far from the only concern.
Specifically in an embedded project, toolchain complications and memory use (both RAM and code) are potentially much bigger concerns than for Desktop applications, and your selection of programmers is more limited as well; might be much more feasible to lock your developers onto acceptable C coding standards than to make e.g. "template metaprogramming" a necessary prerequisite for your codebase and then having to teach your applicants electrical engineering.
Both object oriented programming and compile time computation is doable for a C codebase, just needs more boilerplate and maybe a code-generator step in your build, respectively. But that might well be an advantage, discouraging frivolous use of complexity that you don't actually need, and that introduces hidden costs (understanding, ease of change, compile time) elsewhere.
The substitute for this is that C is insanely easy to generate. Do your compile time computation in your build system.
OO is also pretty trivial in C -- the Linux kernel is a great example of a program in C that is very Object Oriented.
C can't parameterize an optimal layout fixed-length bitset with zero overhead, nor can it pragmatically union error sets at scale.
In practice: Is it worth wrapping all your vendor-provided hardware abstraction in classes that you write yourself? Designing something like that to be "complete" (i.e. allow full access to all hardware functionality like IRQ/DMA setup) and to still stay hardware-independent is often borderline impossible anyway, and any attempt is invariably going to make your codebase more difficult to understand.
Generally, the more you deviate from your vendors "happy path", the more busy work/unexpected difficulties you will run into, and a solid grasp of how exactly architecture and toolchain work might become necessary (while staying on the "happy path" allows you to stay blissfully unaware).
Honestly, I can't think of a single job for which C++ is the best tool.
[0]: https://www.etlcpp.com/
Last time I did embedded work this was basically all that was required.
Suppose Doodads can be constructed from a Foozle either with the Foozle Resigned or with the Foozle Submitted. Using tagged dispatch we make Resigned and Submitted types and the Doodad has two specialised constructors for the two types even though substantively we pass only the Foozle in both cases.
In a language like Rust all the constructors have names, it's obvious what Vec::with_capacity does while you will still see C++ programmers who thought constructing a std::vector with a single integer parameter does the same because it's just a constructor and you'd need to memorize what happens.
For more on disambiguation tags, see https://quuxplusone.github.io/blog/2025/12/03/tag-types/
and https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p39...
And that leads to the ability to use modern C++ methods in places where it is highly useful such as the ability to use "if constexpr" and templates to make your somewhat generic code optimal for a specific usage scenario like writing a templated driver that adapts to the specific hardware variant you are building. This means you can often run without making runtime decisions on hardware situations that cannot ever occur because the specific chip or board does not even have the option.
Also I noticed the virtual interface mentioned and I would express a minor point of caution there for embedded developers. A virtual interface is really meant for dynamic/runtime situations where the object behind the interface can change at runtime. In embedded that is a more rare case. Consider using concepts to define such interfaces and pass the concrete objects as template parameters where it makes sense. I have on several occasions these last four years reduced the binary size overhead of products by doing this as an object accessed through a virtual interface prevents the removal of unused methods from the final binary (they have to be present even with devirtualization happening to reduce the indirection cost).
Also template bloat is real but also a somewhat over-"hyped" problem in embedded. It occurs when the same template is run several times to produce many variations of the same template. In most template programs this will not happen by accident. Mainly avoid making any reoccuring template configuration apart of the type. Instead we now have the option of initializing objects during compile time using constexpr objects. Make your configuration of the object that way (maybe even consteval) and use that in "if constexpr" if possible. All compilers relevant in embedded can manage to put this configuration into the read-only/flash memory and load it in during start-up/boot.
idk man, obviously I don't know much since I don't have my own online book, but templates would not be at the start of my list when selling C++ for bare-metal.
unit suffixes, namespaces, RAII, constexpr, OOP (interfaces mostly), and I like a lot of the STL in avoiding inscrutable "raw loops".
I like the idea of templates, but it feels like a different and specialized skillset. If you are considering C++ from C, why not ease into it?
Just very basic type substitution is one of the most useful uses of templates and is useful in pretty much all software
They’re also useful when you can’t use virtual dispatch. Concepts help a lot in making that tolerable.
Sure they can get stupid complicated and ugly as hell, but you don’t have to do that. Even their basic form is very useful
That said, RAII is probably the must useful thing
I've seen a lot of new people come into my team as juniors, or regular C/C++ engineers that convert to embedded systems. There is a real lack of good, concise resources for them, and the best result I've had is just mentoring them and teaching as we go.
You could look for an intro to embedded systems resource. Or just get a dev kit for something. Go different than the standard Pi or Arduino. Try and get something like a STM32G0 dev kit working and blinking its lights. It's less polished, but you'll have to touch more things and learn more.
If you want, core areas I would suggest to research are the very low level operations of a processor:
* How does the stack pointer work? (What happens to this during function calls?
* How do parameters get passed to functions by value, by reference? What happens when you pass a C++ class to a function by value? What is a deep vs shallow copy of a C++ object, and how does that work when you don't have an OS or MMU?
* Where is heap memory stored? Why do we have a heap and a stack? How do these work in the absence of an OS?
* The Program Counter? (PC register). What happens to this as program execution progresses?
* What happens when a processor boots, how does it start receiving instructions? (This is vague, you could read the datasheet for something like the STM32G0 microcontroller, or the general Arm Cortex M0 core.)
* How are data/instructions loaded from disk to memory to cache to register? What are these divisions and why do we have them?
* Basic assembly language, you should know how loads and stores work, basic arithmetic, checks/tests/comparisons, jump operations.
* How do interrupts work, what's an ISR, IRQ, IVT? How do peripherals like UART, I2C (also what are these?), handle incoming data when you have a main execution thread already running on a single core processor?
Some of this may be stuff you already know, or seem rudimentary and not exactly relevant, but they are things that have to be rock solid before you start thinking about how compilers for differently languages, like C++, create machine code that runs on a processor without an OS.
Assembly is often overlooked, but a critical skill here. It's really not that bad. Often when working with C++ (or Rust) on embedded systems, if I'm unsure of something, my first step is to decompile and dump the assembly to investigate, or step through the assembly with GDB via a JTAG probe on the target processor if the target was too small to hold all the debug symbols (very common).
Anyways, this may have been more than you were asking for. Just me typing out thoughts over my afternoon coffee.
The former is probably more what you are looking for.
- Applied Embedded Electronics: Design Essentials for Robust Systems by J. Twomey. It goes over the whole process making a device and what knowledge would be required for each. Making Embedded Systems, 2nd Edition by E. White is a nice complement.
- Embedded System Interfacing by M. Wolf describes the signals and protocols behind everything. It's not necessary as a book, but can help understand the datasheets and standards
- But you want to start with something like Computer Architecture by C. Fox or Write Great Code - Volume 1 - Understanding the Machine, 2nd Edition by R. Hyde. There are better textbooks out there, but those are nice introductions to the binary world.
The gist is that you don't have a lot of memory and CPU power (If you do, adapting Linux is a more practical option unless it's not suited for another reason). So all the nice abstractions provided by an OS is a no go, so you need to take care of the hardware and those are really finicky,
Using C++ iterator interface to fix the main problem of a standard ring buffer of non-contiguous regions is a cute idea, but I like to use a "bip buffer"[1] instead which actually always gives you real contiguous regions so you can pass the pointers to things like a dma engine.
[1] https://ferrous-systems.com/blog/lock-free-ring-buffer/
The tradeoff is that you have in the worse case only half the buffer available - the ring buffer essentially becomes a kind of double buffer where you periodically switch between writing/reading at the end or the beginning of the storage.