Home > On-Demand Archives > Theatre Talks >
Modern C++ in Embedded Development
Amar Mahmutbegovic - Watch Now - EOC 2023 - Duration: 34:14
Modern C++ in Embedded Development: Compile-Time Observer Pattern
Despite C remaining the dominant language in embedded development, accounting for up to 65% of embedded projects, the adoption of C++ has grown steadily. With an estimated usage of 20% - 30% in the embedded development field, C++ offers classes, improved type safety, and quality of life features such as range-based for loops, the auto keyword, and lambdas.
Modern C++ provides a range of features that enhance the expressiveness and flexibility of codebases in embedded development while maintaining low binary size and memory usage. This session aims to dispel common misconceptions about C++, such as increased binary size and slow runtime performance, through an example implementation of the observer pattern. Attendees will gain insight into the simple yet effective application of Modern C++ for solving common problems in the embedded domain.
Topics covered in this talk will include:
- Debunking embedded developers' misconceptions about C++
- Modern C++ quality of life features: auto, lambdas, and constexpr
- An introduction to templates and fold expressions
- Implementing a compile-time observer pattern in C++
Thanks Nathan. Exceptions and RTTI are the only two features in C++ that don't comply with the zero-overhead principle, and that's why most compilers support disabling them. I always disable them for my embedded projects.
I'd argue that enums in C++ are an improved version of enums in C for several reasons. Enums in C are built upon the implementation-defined integral type, whereas in C++, you can specify the underlying type. In C++, you can't implicitly cast them to ints, which adds to type safety.
Here is a quick list of features to have in mind when using C++ in embedded:
- Disable RTTI and exceptions in your build system using compiler flags,
- If you don't want to use dynamic memory allocation, avoid built-in string type and containers such as vector and map. std::array is fine.
- Virtual methods add some runtime overhead. Avoid them for performance-critical code. Look into alternative approaches such as compile time polymorphism. Look into CRTP pattern.
- Be careful with lambdas. They can add some code bloat and even use dynamic memory allocation. Use them when you get familiar enough to know what's going to happen under the hood. Compiler explorer is your friend when you are not certain about the outcome.
I personally didn't have a situation where C code would act differently in C++. Maybe you can provide an example where this would be a case so we can analyze it.
Thanks for the response Amar. It's great. With that I think I can get ready to start my next project with C++ (I mean if the rest of my team is OK with it of course)
Maybe something like this is what you're looking for?
Thanks for the link, it's exactly what I was looking for !
Informative talk . I will get started on Modern C++ , seems really useful especially for very large projects that needs code reusability , features of OOPs and other features
Thank you Amar
When I was using c++ in the past, I found that templates caused a lot of code bloat. Is this still true in c++17/newer? If so, how do you handle that in your code to prevent it?
It is important to profile your code in cases when you notice it gets bloated. I haven't noticed that my usage of templates makes the binary output bloated, and templates will often allow the compiler to optimize the code in good ways. I tried to demonstrate this in my presentation using the observer pattern. If you take a look at the assembly output, you will see there are no calls to update methods, as the compiler inlined them all.
Thanks for the talk, Amar! Regarding the use of dynamic memory in the STD library, I thought I'd share with anyone looking at the comments that Phil Johnston from Embedded Artistry has a whole online course about using C++ without the heap. I'm a big fan of his work so even though I haven't completed the course myself, I feel confident saying that it's probably very well-done. Also, I had a question for you about C++ vs C. I favor C, though I'm in the process of learning C++, and my biggest complaint is just that C++ is so complicated: the C++17 specifications document is three times as long as the C17 specifications document; people have written documents that are hundreds of pages long just on move semantics; it's possible to make a "Protected Abstract Virtual Base Pure Virtual Private Destructor" (Peter Linden describes it in "Expert C Programming"); the C++ compiler may generate copy/move constructor/assignment operators if some or none are specified by the programmer; etc. What would you say to a person (like me) that thought the overall complexity of the language outweighed any benefits of the quality of life features you mention?
Thanks, Nathan! C++ without a heap doesn't mean you can't use STD algorithms and containers. std::array is perfectly fine. In order to be extra sure, you can override new and delete operators and gracefully terminate the application in case that dynamic allocation happens. Turning off exceptions and RTTI is a smart idea, and I use it as well.
You are absolutely right. C++ is a huge language. And I often say there are many dialects of C++. Mine dialect is no allocation when on very resource-restricted MCUs, which means I am not using any containers except std::array. I also don't use move semantics.
A simple argument for people like you would be to start with quality of life features. You have absolutely nothing to lose. Auto, lambdas, std::optional, and structured bindings, just to name a few. Constexpr is not just a quality of life feature. It's way more than that. Make sure you watch Ben Saks' talk Compile-Time Programming in C++ Using Constexpr.
Start small and keep discovering C++ and the features that are useful for your type of work. It was a journey for me. Templates may look scary at first, but when you discover their real potential, they are a wonderful thing that allows compile-time configuration for different platforms, etc. Constexpr is another wonderful feature that gets things done for you in compile time.
Conference talks were my main source of inspiration. Michael Caisse has some wonderful talks. Ben Salks, Odin Holmes, and Dan Salks also have great talks.
The subset of C++ that I'm using greatly improved the quality and readability of my code. Nowadays, it's really hard for me to work on a project if I am restricted to C only. It is a huge language, and you need to find a subset that works for you. The learning curve is steep, but it pays off.
Thanks for the reply, Amar! So I'm hearing you say "Yes, C++ is complicated, but if you're already familiar with C then the easiest way to slowly get used to that complexity is to treat C++ like 'C with...' (C with classes, C with constexpr, etc). That way you only use the features that are useful and that you understand until you feel comfortable using them." Do I have that right?
I've got one follow-up question, since I'm genuinely curious about your opinion: part of my original concern was that the C++ compiler is doing so many things for me that the final binary might not be recognizable to me (such as creating move constructors for me, or something like that) or that I'm able to write a line of C++ that makes sense to me but which is interpreted by the compiler in a very different way. I'm worried that using the C++ compiler would be like using a sensor library that I didn't fully understand, in that if something doesn't work then my debugging gets stymied because I never fully understood how the tool works. Have you ever run into this problem (of writing C++ code that was compiled into something unexpected) or am I dreaming it up?
That's right Nathan. That's how I embraced it. I started using it as "C with X", and kept expanding my X over time. It's a very natural way for a C developer to transition to the C++ dialect of his choice.
Well, I heard that argument a lot, the compiler is doing something under the hood and hiding behavior. The compiler will generate a default constructor, copy constructor, copy-assignment operator, and destructor for a type if it does not declare its own. These might look like strange concepts to a C developer, and there is some learning curve to take a grasp of them. I personally use them rarely as they are related to objects.
I rarely make instances of objects. This may sound weird to you, but the thing is that you benefit more from types (classes) than from objects. Take a look at my example with templated Sensor class. You pass a type I2C as a template argument, and you don't make an instance of it. This approach is inspired by Wouter van Ooijen's talk "Objects? No Thanks! ". It was life-changing for me.
When it comes to compiler output, compiler explorer is your friend when discovering C++. Make a simplified example and see what the compiler makes out of it. The zero-overhead principle states that you don't pay for what you don't use, and what you do use is just as efficient as what you could reasonably write by hand.
Great talk, thanks. I have a question for you : when considering C++ for a new project, I am always afraid of two thing: 1. that I may bring it some features that I don't want or that makes the binary larger (eg exceptions or RTTI) and 2. that some of the code I would have written in C may act differently under a C++ compiler (eg: enums being cast to int in C but not in C++). Do you have a quick list of features to be wary about in C++ ? and have you been beaten by C code acting differently in C++ without noticing / knowing it in the first place ?