C++ On Embedded Systems

Article by:
Date Published:
Last Modified:

Overview

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:

  • Classes
  • 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. Also be careful of overusing them, too much templating makes code very unreadable.
  • Function overloading and default parameters - Makes it easy to extend functions without massive refactors! And provides more flexibility to an API.
  • Enum classes, typesafe typedefs
  • Operator overloading (when done sensibly!)
  • References (they are just safer pointers that can’t be null!)
  • Namespaces (no overhead)
  • Inheritance
  • auto
  • Smart pointers (std::unique_ptr, std::shared_ptr)
  • std::array

These are explained in more detail in the below sub-sections.

Classes

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_Write(), 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.

Templates

Templates allow you to create slightly different versions of classes that use different types at compile time.

Be careful not to overuse templates. When used too heavily they can make code very hard to read. Just to show how far you can take templating and metaprogrmming, someone wrote an entire compile-time ray tracer. Just because you can, doesn’t mean you should 😊.

Enums Classes

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. GPS_IDLE and MOTOR_IDLE.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// C-style enums
enum EStates { OFF, IDLE, RUNNING };
enum EColors { RED, BLUE, GREEN };

// C++-style enum classes
enum class ECStates { OFF, IDLE, RUNNING };
enum class ECColors { RED, BLUE, GREEN };

int main() {
  EStates myEState;
  myEState = IDLE;
  if (myEStateState == GREEN) { // Bad idea, but compiles
    // ...
  }

  ECStates myECState = ECStates::IDLE;
  if (myECState == ECColors::GREEN) { // Bad idea, but yay, won't compile!
    // ...
  }
}
TIP

While enum class won’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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum GpsState {
  OFF
  IDLE,
  SCANNING
};

enum MotorState {
  OFF, // Because of the global scope, this is going to collide with the OFF in GpsState!  
  RUNNING
};

But if we use enum class we avoid the naming collision:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum class GpsState {
  OFF
  IDLE,
  SCANNING
};

enum class MotorState {
  OFF, // This is fine, MotorState::OFF is not going to collide with GpsState::OFF
  RUNNING
};

Namespaces

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.

1
2
3
namespace my_namespace {
  // All my stuff goes here
}

std::array

std::array is a wrapper around a C-style array that provides more safety around it’s usage. Unlike in C, it does not decay to a pointer to the element type automatically1.

If you use the [] operator to access the elements, it behaves the same as in C, with no bounds checking:

1
2
std::array<uint8_t, 3> myArray;
myArray[3] = 10; // Woops, no bounds checking!

However, if you use .at() instead, you get bounds checking:

1
2
std::array<uint8_t, 3> myArray;
myArray.at(3) = 10; // Bounds checking, this will throw an exception

std::out_of_range is thrown if the index is out of bounds.

You also get a useful .fill() function, to fill all the elements with a single value:

1
2
// Set the array to all 0's
myArray.fill(0);

Another useful feature if that you can copy an entire array by just using the = operator:

1
2
3
4
5
6
7
8
9
std::array<uint8_t, 3> myArray1 = {1, 2, 3};
std::array<uint8_t, 3> myArray2;
myArray2 = myArray1;

Note this only works with arrays of the same type AND same size.

```c++
std::array<uint8_t, 4> myArray3;
myArray3 = myArray2; // Compiler error! Different sizes

C++ Features That Could Be Used, But Only After Careful Consideration

  • new/delete
  • Virtual Methods
  • RTTI
  • Exceptions
  • std::string
  • std::vector
  • Boost

new/delete

new and delete is C++’s answer to C’s malloc() and free() functions. They are improved versions of malloc() and free().

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

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.

std::string

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <cstdint>

class TempSensor {
public:
  TempSensor(uint8_t i2cAddress) {
    // Validate I2C address
    if (i2cAddress > 127) {
      // Argh, error! What do we do in firmware when we can't throw an exception?
    }
    this->i2cAddress = i2cAddress;
  }
private:
    uint8_t i2cAddress;
};

int main() {
  TempSensor tempSensor(10); // We don't know if this is ok or not!
}

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.

NOTE

Runnable code for all this examples can be found at https://replit.com/@gbmhunter/cpp-handling-constructor-errors.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <cstdint>
#include <cstdio>

class TempSensor {
public:
  TempSensor() {
    // Nothing here now
  };

  uint8_t Init(uint8_t i2cAddress) {
    // Validate I2C address
    if (i2cAddress > 127) {
      return 1;
    }
    this->i2cAddress = i2cAddress;
    return 0;
  };
private:
    uint8_t i2cAddress;
};

int main() {
  TempSensor tempSensor;
  uint8_t status;
  
  status = tempSensor.Init(10); // OK, returns 0
  status = tempSensor.Init(200); // Error, returns 1  
}
TIP

You might want to make sure the caller does not forget to call the Init() function before calling any other functions. This can be done by adding a bool m_isInitialized variable to the class which then gets checked upon entry to all other member functions.

Using an 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <cstdint>
#include <cstdio>

class TempSensor {
public:
  TempSensor(uint8_t i2cAddress, uint8_t & status) {
    // Validate I2C address
    if (i2cAddress > 127) {      
      status = 1;
      return;
    }
    m_i2cAddress = i2cAddress;
    status = 0;
  }
private:
    uint8_t m_i2cAddress;
};

int main() {
  uint8_t status;
  TempSensor tempSensor(200, status);
  if (status != 0) {
    printf("Error initializing temp. sensor. status=%u\n", status);
  }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <cstdint>
#include <cstdio>

class TempSensor {
public:
  TempSensor(uint8_t i2cAddress) {
    // Validate I2C address
    if (i2cAddress > 127) {
      // Returning here will mean m_initSuccessful will stay false
      return;
    }
    m_i2cAddress = i2cAddress;
    m_initSuccessful = true;
  }

  bool initSuccessful() {
    return m_initSuccessful;
  }
private:
  bool m_initSuccessful = false;
  uint8_t m_i2cAddress;
};

int main() {
  TempSensor tempSensor(10);
  if(!tempSensor.initSuccessful()) {
    printf("Initialization of temp. sensor failed.");
  }
}

The boolean 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:

1
2
3
4
5
6
7
// In a .cpp file somewhere...
extern "C" {
    void SPI1_IRQHandler(void)
    {
        // ISR handler code goes here
    } 
}

If you didn’t wrap SPI1_IRQHandler(void) in 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:

1
2
3
4
int main(void) {
    // In your firmware app somewhere...
    register_isr(&SPI1_IRQHandler);
}
NOTE

C++ performs name mangling so it can support function overloading (functions with the same name, but supporting a different number and/or type of input and output variables).

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.

Further Reading

The document “Technical Performance on C++ Performance” is a good read if you are really interested in the advantages/disadvantages of using C++ on an embedded platform2.

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.

References


  1. cppreference.com. std::array. Retrieved 2024-03-15, from https://en.cppreference.com/w/cpp/container/array↩︎

  2. 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↩︎


Authors

Geoffrey Hunter

Dude making stuff.

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License .

Related Content:

Tags

comments powered by Disqus