Embedded C++: What's the value?

20180709 Update: Fixed Software References link and typos

It's not uncommon to hear this statement: "C++ just doesn't work well for embedded systems." Often, there will be vague, sweeping reasons such as:

  • C code is going to be smaller than C++!
  • C++ is totally going to blow up your stack / heap allocations!
  • C++ is a powerful tool - and you have to be wary of shooting yourself in the foot! Those features can cause code bloat!
  • C++ won't work well if you are using an 8-bit microcontroller!
  • Exceptions and RTTI make the output so much bigger and make things take longer!

I was also in the C-only camp - I thought that C++ would penalize me in size and speed. I was forced to confront my ignorance as we converted Pearl's RearVision firmware from C to C++. I saw the power, convenience, and speed that C++ offered firsthand and the outcome assuaged my fears: the firmware binary is 1.6MB, even with RTTI and exceptions enabled in certain objects.

Here are some things that didn't sink in until I started working with C++:

  1. If you are space/memory constrained, you can ruin yourself in the same way with C and C++. C is not inherently safe.
  2. C and C++ are different languages. You must understand your tools to apply them correctly.
  3. C++ was designed to be competitive in performance with C.
  4. There are many valuable features of C++ that do not cost you anything.
  5. You can use or selectively disable features you don't need.
  6. RAII and SBRM are extremely helpful patterns that are easier to utilize in C++.
  7. Many teams recreate C++-like object models and vtables in C - the compiler can do it more efficiently than you can.

Your next question is likely "ok, fine - what's so great about C++ then?" My short answer: C++ helps me write neater, more explicit, and less error-prone code than C does.

I've included a summary of C++ features that I utilize on a daily basis, grouped into two sets: those that help me write higher quality code and those that work well for embedded systems. Over the next few weeks I will be exploring these C++ features, including examples, tricks, and pitfalls to avoid.

In the meantime, if you're looking for more information on C++, check out my Software References page.

Code Quality

  • Stronger type safety
    • More explicit casting rules
    • nullptr
    • Virtual functions and templated types allow me to eliminate void * usage
    • Passing arguments by reference can replace many error-prone uses of pass-by-pointer in C.
      • const can allow you to pass-by-reference (eliminating a copy) but still indicate no modifications are allowed.
  • Function overloading
    • Enables neater code - interfaces that provide the same functionality but use different types can be grouped together, allowing for easier logical understanding of the functionality
  • auto
  • Smart pointers
    • std::unique_ptr and std::shared_ptr can eliminate many memory bugs that result from missing a free() call
  • Better utilization of scope-based resource management
    • Take a lock, and ensure that a lock is released at all scope exit points automatically (never forget to unlock() before return!)
    • Ensure memory is freed properly at all scope exit points
  • Templates
    • reduce repetition and generalize APIs, especially when differentiated primarily by type
    • Stronger type-safety over C-style macros
    • Enables more compile-time checking / computation

Embedded-Friendly Features

  • constexpr
    • Indicate to the compiler that you intend for a constant/function to act as a compile-time constant
    • Enables compiler optimization
  • std::align, std::aligned_storage, std::alignment_of
    • std library interfaces for aligning memory
  • Ability to custom allocators/deleters
    • Works with smart pointers too - safely pass around memory without worrying about whether to call free() or aligned_free()
  • Virtual functions/classes
    • Cost is minimal: one vtable per class, one pointer per object, one dereference per function call. Not slow!
    • Uses:
      • Defining interfaces
      • Abstract objects using the same interface (e.g. operate on an array of SPI devices)
  • Templates
    • Eliminate dead code - only use versions of a function that are required at compilation time (useful for safety-critical systems). Extra code is not ever generated.
  • std::mutex
    • Provides a useful wrapper for your RTOS mutex
    • Implementing std::mutex allows you to utilize SBRM and RAII type features such as std::lock_guard.
  • std::array
    • Statically allocate memory, defined at compile time