One fear about using C++ on an embedded system is a decrease in performance (in terms of memory and processing speed). As with most complex issues, the answer really is – “it depends”. Also, the concept on what an embedded system various between people and companies. You can use most C++ features on high-level embedded devices that are running a full-blown OS such as Linux. What this page refers to is “deeply” embedded systems – systems which are running “bare metal” (no OS) or with an RTOS on a microcontroller. Memory is normally very constrained, typical flash memory sizes are between 16-256kB, and RAM between 2-64kB. The CPU clock speed might be anywhere from 8 to 250MHz, with typically a single core.
I believe you can carefully select a subset of the C++ language which provides most of the benefits of OO-based design, but does not incur any significant performance hits for the target embedded system.
This page aims to cover many of C++’s features and weigh-in on their suitability in deeply embedded systems.
C++ Features That Should Be Used
This is a list of all the C++ features that you SHOULD USE in most embedded firmware:
- Templates (no overhead, can be thought of as a more powerful version of a macro). However, incorrect/careless use of templates can cause a huge increase in code size.
- Function overloading and default parameters. No overhead.
- Enum classes, typesafe typedefs
- Operator overloading (when done sensibly!)
- References (they are just safer pointers that can’t be null!)
- Namespaces (no overhead)
- Smart pointers (
These are explained in more detail in the below sub-sections.
If you’ve ever done a C-based embedded project and had multiple instances of an peripheral to control (e.g. a UART), you’ve probably realized it’s inefficient and hard to main the code if you just copy all your
Uart_Write(char * bytes, ...) and
Uart_Read() functions and call them
Uart2_Read() e.t.c. You then probably thought, hey, I’ll just have one copy of all the functions, but for all of them as the first parameter, pass in a Uart
struct which contains all the configuration and state data for a particular UART.
Now your functions are looking something like
Uart_Write(Uart& uart, char * bytes, ...). Well guess what, this is the basic idea of a class in C++, but in a more readable and maintainable way. So there is no reason not to use C++ classes in embedded firmware, and there is no performance penalty, at least when classes are used in this basic sense.
Enums in C can be dangerous. A user of a library function can pass any old integer value as a parameter into a function which takes an enum. The compiler will not complain, however at runtime the provided value could be completely out-of-range and cause unexpected bugs.
Also, all enums share the same scope, so you have to be careful to avoid name collisions! For example, if you want to have two
IDLE enums for different state machines, you are forced to change the names, e.g.
C++ improves things with the concept of the
enum class. This is a more strongly typed enum, and the compiler will error if you try and convert between this type and an integer without explicitly doing a cast. The enum values are also scoped to the name of the enum, preventing naming collisions with other enums in your project.
The example below shows how C-style enums can be implicitly cast to integers (which can be dangerous), whilst an
enum class won’t.
enum classwon’t let you implicitly cast to an integer, it’s still sometimes useful to be able to do so. You can still do it, but you have to explicitly cast the variable in your code. I recommend using
static_cast<uint8_t>(myEnum)to do so.
The below example shows how C-style enums can cause scoping/naming issues.
But if we use
enum class we avoid the naming collision:
C++ namespaces are a great feature to use to help you organise your code. They allow you to organize your code into logical scoped groups and prevent naming collisions. This is especially important when writing libraries, as from the library writers perspective, they have no control over the naming of everything else in the projects it will be used in, and from the library users perspective, they really don’t want to be diving into library code to change things to fix naming collisions if at all possible.
C++ Features That Could Be Used, But Only After Careful Consideration
- Virtual Methods
delete is C++’s answer to C’s
free() functions. They are improved versions of
My general rule for embedded firmware is, unless otherwise banned from using dynamic memory allocation due to a specific coding standard (e.g. MISRA), to allow the use of dynamic memory allocation, but only during initialization. My opinion is that this gives the best of both worlds. The ability to use dynamic memory allocation allows your firmware to be flexible to initialization variables and configuration. Only allowing it an initialization prevents the “memory segmentation” issues that can occur when objects are continually created and deleted from the heap at runtime.
Exceptions are powerful, they allow errors to propagate up a stack until you care about catching them, without having to add any code at any of the layers which don’t care about the exception. However, they are a problem for most deeply-embedded firmware projects, because:
- Stack unwinding is processor intensive.
With exceptions not allowed in embedded firmware, error handling is typically done by returning error codes from functions and checking them every time the function is called. This may seem like a lot of hassle at first, but is a very common paradigm and forces you to think about the “exceptional” scenarios and how to handle them. Many more modern languages such as Go and Rust encourage the “return error code, check error code, continue” process over exceptions anyway.
When exceptions are disabled, one issue to think about is how to handle errors in constructors. See the Handling Errors In Constructors section below on that.
The GCC compiler supports the
-fno-exceptions flag which disables exceptions.
I would generally recommend against using
std::string in embedded firmware, due to it’s use of dynamic memory allocation at runtime (when using the default allocator).
In embedded C++,
std::string_view can be used as a safer wrapper around raw NTBS (null-terminated byte strings) without using dynamic memory allocation at runtime.
Handling Errors In Constructors
One issue when writing classes in firmware is the difficulty in signalling back to the caller when an error occurs in a C++ class constructor. The standard function approach of returning a error number does not work as you are not allowed a return value from C++ constructors. When developing C++ applications running on a full-blown OS, the standard way to deal with it is to throw an exception. In an embedded application where you are not allowed to use exceptions, this is obviously not an option. For example, consider the following example class:
You want to check in the constructor if the caller has provided a valid I2C address (one with the range 0-127). There are a few ways you can signal this fault back to the caller, and we’ll discuss these now.
Move All Non-Trivial Code To Init()
One popular way of getting around this problem is to move all non-trivial might-cause-an-error code out of the constructor and into an
Init()function before calling any other functions. This can be done by adding a
bool m_isInitializedvariable to the class which then gets checked upon entry to all other member functions.
Init() function also opens up the possibility of the caller being able to re-initialize the class without having to delete this object and construct a new one. This may or may not be beneficial for a particular application!
One downside to this approach is that the caller has to remember to call
Init() before they can use any of the classes other functions. Also, adding the
m_isInitialized check is more code that you have to add to every member function.
Pass In Status Pointer To Constructor
Another way to solve the problem is to pass into the constructor a pointer to a status variable so the constructor can assign to it, similar to how you would pass in extra “outputs” to a normal function if the function had more than one output.
Provide Second Function Or Member Variable To Check If Constructor Worked
The final way we will solve the constructor error problem is to save the constructor success state to the class, and then provide the caller with another function to get that state:
m_initSuccessful could be changed to a
uint8_t if you want to provide the caller with more detailed information as to why the constructor failed.
C++ And Interrupts
There is a few things to be aware of when using C++ for embedded firmware projects that use interrupts and interrupt service routines (ISRs):
C++ Mangling The ISR Name
C++ mangles function names (adds arbitrary characters to the name) – If your firmware environment relies on exact function names for ISRs, then you cannot use a C++ function. The work around for this is to wrap the ISR function with
extern "C". The following example is using the STM32 standard peripherals firmware library:
If you didn’t wrap
extern "C", it’s compiled name would be something like
AbXllGSPI1_IRQHandler(void). This would not get picked up by the linker, and because in the startup code there is a default (fall-back) IRQ function defined with
__weak, you wouldn’t even get a error saying the function is missing!
You won’t have this problem if the platform you use provides a function to “register” ISRs, as you’ll be passing the mangled name to the register function anyway:
ISRs Calling Class Functions
ISRs typically have a strict function type signature – no input variables and no return type. Thus there is no way to pass through as function inputs instances of C++ classes to access and call class member functions. For this reason you generally have to define file scoped class instances that the ISR has access to (or a static member function, or just call a C-style function). See the C++ Callbacks page for more info.
The Embedded C++ Homepage is sort of a hub for embedded C++ programming. They define a sub-set of the full C++ language for use on embedded devices such as microcontrollers.
C++ Standard Libraries For Embedded Devices
uClibc++ is a C++ standard library designed specifically for microcontrollers. It even has exception support!
Check out The Standard Template Library (STL) For AVR With C++ Streams if you want to get a library for using things like string and iostream with AVR microcontrollers.
ISO/IEC (2006, Feb 15). ISO/IEC TR 18015:2006(E) - Technical Report on C++ Performance. Retrieved 2022-09-29, from https://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf. ↩︎
This work is licensed under a Creative Commons Attribution 4.0 International License .
- dynamic memory allocation