In this lesson:
- what C++ adds over C, and which parts belong on an MCU
- the zero-overhead principle, why a class method can compile to the exact same code as a C function
- the embedded-appropriate C++ subset (and what to leave off)
- name mangling and
extern "C"for linking C and C++ together - the stricter type rules that catch bugs C lets through
C++ is (mostly) C plus abstractions
You already know C. C++ began as "C with classes" and stays almost a superset: most valid C compiles as C++. On top of C it adds classes, templates, references, namespaces, operator overloading, constexpr, RAII, and a stronger type system.
The fear embedded engineers have, "C++ is bloated and slow", is mostly about features, not the language. Used as an embedded subset, C++ compiles to machine code as tight as hand-written C, while giving you safer, more expressive abstractions.
The zero-overhead principle
Bjarne Stroustrup's design rule for C++: what you don't use, you don't pay for; and what you do use, you couldn't hand-code any better. A class method isn't a heavyweight construct, it's a function with an implicit this pointer.
A GPIO toggle in C and in C++ compile to identical instructions:
/* C: a function operating on a struct */
typedef struct { volatile uint32_t *odr; uint32_t pin; } Led;
void led_toggle(Led *self) {
*self->odr ^= (1u << self->pin);
}
led_toggle(&led); /* explicit self */// C++: a method on a class β same machine code
class Led {
volatile uint32_t *odr_;
uint32_t pin_;
public:
void toggle() { *odr_ ^= (1u << pin_); } // 'this' is the implicit self
};
led.toggle(); // 'this' passed implicitlyled.toggle() and led_toggle(&led) generate the same instructions, the C++ version just moves the self pointer from explicit argument to implicit this. No virtual dispatch, no overhead. That's the whole bargain: better organization, identical cost.
The embedded-appropriate subset
Not all of C++ is MCU-friendly. The split:
| Use freely (zero/low overhead) | Avoid on MCUs (cost or non-determinism) |
|---|---|
| Classes, methods, encapsulation | Exceptions (throw/try), code size + non-determinism |
| RAII (deterministic cleanup) | RTTI (dynamic_cast, typeid), metadata bloat |
| Templates (compile-time generics) | Dynamic allocation (new/delete), fragmentation |
| References | <iostream>, enormous code size |
const / constexpr |
Most of std:: that allocates (std::vector, std::string) |
enum class |
Heavy standard library / std::function |
This module teaches the left column and dedicates a full lesson to why the right column is dangerous on constrained targets. The good news: the valuable abstractions (classes, RAII, templates) are exactly the cheap ones.
Compiling and linking: g++, mangling, extern "C"
C++ is compiled with g++ (or arm-none-eabi-g++) and source files end in .cpp. The pipeline is the same four stages as C, with one twist that bites at link time: name mangling.
Because C++ allows overloading (two functions named init with different parameters), the compiler encodes the parameter types into the symbol name, init(int) might become _Z4initi. C does no such thing; its symbol is just init. So a C++ file calling a C function, or vice versa, needs extern "C" to disable mangling for that interface:
// In a C++ file, tell the compiler these use C linkage (no mangling),
// so they link against C object files and the linker can find them.
extern "C" {
#include "uart.h" // C header
void HardFault_Handler(void); // ISR the C startup code references by name
}This is essential in real firmware: vendor HALs, RTOS kernels, and startup code are usually C, and your extern "C" wrappers let C++ application code link against them. Interrupt handlers in particular must keep their unmangled names so the vector table (C) finds them.
Stricter types catch more bugs
C++ tightens several of C's loose rules, which means some valid C is rejected by C++, usually because it was risky:
// 1. void* does NOT implicitly convert to a typed pointer in C++.
int *p = malloc(n * sizeof(int)); // ERROR in C++ (OK in C)
int *p = static_cast<int*>(malloc(n * sizeof(int))); // explicit cast required
// 2. 'bool' is a real type, not a typedef'd int.
bool ready = true; // distinct type, size 1
// 3. enum class is strongly typed β no implicit int conversion.
enum class Mode { Idle, Run };
Mode m = Mode::Run; // can't accidentally compare to 7
// 4. stricter const rules and function prototypesThese catch real defects at compile time, the kind of implicit-conversion bugs that slip through C. The tradeoff is you write a few more casts; the payoff is the compiler rejecting nonsense.
Gotchas
- C++ isn't automatically bloated, features are. A class with non-virtual methods costs nothing extra. Pulling in
<iostream>or exceptions does. Bloat comes from what you use, so choose the subset deliberately. - Forgetting
extern "C"breaks C/C++ linking. Without it, the C++ compiler mangles names and the linker can't match them to C definitions, you get "undefined reference." Wrap C headers and ISRs inextern "C". - Some valid C won't compile as C++.
void*needs an explicit cast,new/class/boolare keywords you can't use as identifiers, and implicit narrowing is stricter. Porting C to C++ is mostly mechanical but not zero-effort. .cfiles still compile as C even with g++. The compiler picks the language by extension; rename to.cpp(or force-x c++) or your "C++" file is just C.
TL;DR
- C++ is nearly a superset of C, adding classes, templates, references, RAII, namespaces, and stronger types.
- The zero-overhead principle means the good abstractions (classes, RAII, templates) compile to the same code you'd hand-write in C,
led.toggle()β‘led_toggle(&led). - The embedded subset embraces classes/RAII/templates/
constexpr/references and avoids exceptions, RTTI, dynamic allocation, and the allocating standard library. - C++ mangles function names to support overloading; use
extern "C"so C++ links with C HALs, RTOSes, and ISR vector tables. - C++ rejects some loose C (implicit
void*conversion, weak enums), trading a few explicit casts for compile-time bug catching.