Quick Cap
C++ offers zero-cost abstractions, RAII, and type safety that make it appealing for embedded systems -- but the full language carries runtime costs (exceptions, RTTI, heap allocation) that are unacceptable on resource-constrained targets. Embedded C++ is about knowing which features to keep and which to disable, so you get the safety benefits without the overhead.
Interviewers test whether you understand the concrete costs of each disabled feature, which STL components are heap-free, and how to interoperate with C code in mixed firmware projects.
Key Facts:
- Exceptions disabled (
-fno-exceptions): Exception handling adds unwinding tables (10-30% flash overhead) and non-deterministic latency -- both unacceptable in real-time systems. - RTTI disabled (
-fno-rtti): Runtime type information adds per-class metadata to flash. Without RTTI,dynamic_castandtypeidare unavailable. - Heap-free programming:
new/deleteare replaced by static allocation, placement new, and fixed-size memory pools to avoid fragmentation and non-deterministic allocation time. - Safe STL subset:
std::array,std::optional,std::string_view,std::bitset, and<algorithm>are heap-free and safe.std::vector,std::string,std::map, and<iostream>allocate from the heap and are avoided. extern "C": Required for calling C functions from C++ and exposing C++ functions to C linkers -- prevents C++ name mangling.- Industry standards: MISRA C++ 2023 and AUTOSAR C++14 define which C++ features are permitted in safety-critical embedded code.
Deep Dive
At a Glance
| Aspect | Embedded C++ Practice |
|---|---|
| Exceptions | Disabled (-fno-exceptions); use error codes or std::expected |
| RTTI | Disabled (-fno-rtti); use static_cast and compile-time polymorphism |
| Heap allocation | Forbidden or tightly controlled; static allocation preferred |
| STL containers | std::array, std::optional, std::bitset only; no vector, map, string |
| C interop | extern "C" on all shared headers; guard with #ifdef __cplusplus |
| Standard version | C++14 (AUTOSAR baseline), C++17 (constexpr if), C++20 (concepts) |
| Coding standards | MISRA C++ 2023, AUTOSAR C++14, CERT C++, JSF AV C++ |
Why C++ in Embedded?
C++ is not a replacement for C in embedded -- it is C with opt-in abstractions. The features that matter in firmware are all zero-cost at runtime:
- RAII -- Automatically releases resources (locks, peripherals, DMA handles) when scope exits, even on early return. Eliminates an entire class of resource leak bugs.
- Type safety --
enum classprevents implicit integer conversion. Strong typedefs catch unit mismatches (milliseconds vs microseconds) at compile time. - Templates -- Generate specialized, optimized code at compile time with no virtual dispatch overhead. A
RingBuffer<uint8_t, 64>compiles to the same code as a hand-written C ring buffer. - constexpr -- Moves computation to compile time. CRC tables, baud rate divisors, and pin configurations are computed by the compiler, not at runtime.
The key insight: C++ lets you raise the abstraction level without raising the runtime cost, as long as you avoid the features that carry hidden overhead.
Disabled Features: Exceptions
Exception handling (try/catch/throw) is disabled in virtually all embedded C++ projects via the -fno-exceptions compiler flag. The costs are concrete:
| Cost | Detail |
|---|---|
| Flash overhead | Unwinding tables add 10-30% to binary size -- unacceptable on a 64 KB MCU |
| Non-deterministic latency | Stack unwinding time depends on call depth; violates hard real-time guarantees |
| Heap dependency | Some exception implementations allocate on throw |
| No partial adoption | One throw in a call chain forces unwinding tables for the entire call graph |
When exceptions are disabled, throw becomes a call to std::abort() and try/catch blocks are compile errors. This is the norm in automotive (AUTOSAR), aerospace (DO-178C), and bare-metal firmware.
Error Handling Without Exceptions
With exceptions off, you need explicit error propagation. Three patterns dominate:
Pattern 1: Error codes (universal)
enum class Status { Ok, Timeout, CrcError, Busy };Status sensor_read(uint8_t addr, uint16_t& value) {if (!bus_ready()) return Status::Busy;// ... perform read ...if (timed_out) return Status::Timeout;value = raw;return Status::Ok;}
Pattern 2: std::expected (C++23) or custom Result type
std::expected<T, E> carries either a value or an error, forcing callers to check before accessing the value. On pre-C++23 codebases, teams write a lightweight Result<T, E> that does the same thing with no heap allocation.
Pattern 3: Callback-based error reporting
Register an error handler at init time; functions call it on failure. Common in RTOS-based designs where a central error manager logs and decides whether to reset.
If asked about error handling without exceptions, mention std::expected even if the project uses C++14. It shows you understand where the language is heading and that the "error codes are clunky" complaint has a modern answer.
Disabled Features: RTTI
Runtime Type Information (-fno-rtti) is disabled because it stores per-class type metadata in flash. Without RTTI:
dynamic_castis unavailable -- usestatic_castwhen the type is known at compile time, or implement a manual type tag (an enum field in the base class).typeidis unavailable -- use template specialization or tag dispatch instead.- Virtual functions still work -- vtables do not depend on RTTI.
Most embedded C++ code uses compile-time polymorphism (templates, CRTP) instead of runtime polymorphism, making RTTI irrelevant.
Heap-Free Programming
On MCUs with 8-64 KB of RAM, dynamic allocation is dangerous: malloc fragmentation can exhaust memory after hours of operation, and allocation time is non-deterministic. Embedded C++ eliminates the heap entirely:
- Static allocation -- All objects have static or automatic (stack) storage duration. No
new, nodelete. - Placement new -- Constructs an object in a pre-allocated buffer. Useful for initializing objects in a memory-mapped region or a static byte array.
- Fixed-size memory pools -- A pool allocator hands out fixed-size blocks from a static array. Allocation is O(1) and fragmentation-free.
- Stack allocation -- Local objects are allocated on the stack automatically. Keep them small; embedded stacks are typically 1-4 KB.
// Placement new: construct a Sensor object in a static buffer#include <new>alignas(Sensor) static uint8_t sensor_buf[sizeof(Sensor)];Sensor* sensor = new (sensor_buf) Sensor(config);// Must call sensor->~Sensor() manually -- no delete
Some teams override operator new to call a pool allocator, but this silently changes the semantics of every allocation in the program, including inside third-party libraries. A safer approach is to delete operator new entirely so any accidental heap allocation is a compile error.
STL Safe Subset
Not all of the Standard Template Library is off-limits. The key question is: does it allocate from the heap?
| Component | Heap-Free? | Embedded Safe? | Notes |
|---|---|---|---|
std::array | Yes | Safe | Fixed-size array with bounds checking via .at() |
std::optional | Yes | Safe | Nullable value without pointers; no heap |
std::string_view | Yes | Safe | Non-owning view of a string; zero-copy |
std::bitset | Yes | Safe | Fixed-size bit array; replaces manual bitmasks |
std::tuple | Yes | Safe | Fixed-size heterogeneous container |
std::pair | Yes | Safe | Two-element tuple |
<algorithm> | Yes | Safe | std::sort, std::find, std::copy operate on iterators, not containers |
<numeric> | Yes | Safe | std::accumulate, std::inner_product |
std::variant | Yes | Safe (C++17) | Type-safe union; replaces raw unions |
std::vector | No | Avoid | Heap-allocated dynamic array |
std::string | No | Avoid | Heap-allocated; use std::string_view or fixed char[] |
std::map / std::unordered_map | No | Avoid | Heap nodes; use sorted std::array + binary search |
std::shared_ptr | No | Avoid | Reference count block is heap-allocated |
<iostream> | No | Avoid | Pulls in locale, heap buffers, and 50-100 KB of flash |
std::array vs C arrays: std::array is a zero-overhead wrapper around a C array. It adds .size(), .at() (bounds-checked), and works with <algorithm>. There is no reason to use raw C arrays in C++ embedded code.
std::optional vs null pointers: std::optional<T> stores the value inline (no heap), makes the "no value" case explicit in the type system, and eliminates null pointer dereference bugs.
C/C++ Interop: extern "C"
Most embedded projects mix C and C++ -- vendor HALs and RTOS kernels are written in C, application code in C++. The bridge is extern "C", which tells the C++ compiler to use C linkage (no name mangling) for the enclosed declarations.
Calling C from C++ (most common):
// In a C++ source file, wrap the C header includeextern "C" {#include "vendor_hal.h" // C header with C linkage#include "freertos/task.h"}
Exposing C++ functions to C callers:
// In a C++ header, guard the declarations#ifdef __cplusplusextern "C" {#endifvoid app_init(void); // Callable from C startup codevoid app_main_loop(void); // Callable from C main()#ifdef __cplusplus}#endif
The #ifdef __cplusplus guard is essential -- without it, a C compiler will choke on the extern "C" syntax, which is a C++ keyword.
Name mangling explained: C++ encodes function signatures into symbol names to support overloading (_Z10sensor_readhRt). C does not mangle names (sensor_read). Without extern "C", the linker cannot find C functions called from C++ because it searches for the mangled name.
Compiler Flags for Embedded C++
| Flag | Effect | Why |
|---|---|---|
-fno-exceptions | Disables exception handling | Saves 10-30% flash, deterministic latency |
-fno-rtti | Disables runtime type information | Saves flash, disables dynamic_cast/typeid |
-fno-threadsafe-statics | Disables mutex guards on local static initialization | Avoids pulling in pthread on bare-metal; safe if you init statics before threading starts |
-fno-use-cxa-atexit | Disables __cxa_atexit for static destructor registration | Static objects on MCUs are never destroyed; saves code and RAM |
-fno-unwind-tables | Removes .eh_frame unwind tables | Further reduces flash after disabling exceptions |
-nostdlib | Does not link standard C/C++ libraries | Full control over startup code and memory layout |
A typical embedded C++ compile line:
arm-none-eabi-g++ -mcpu=cortex-m4 -mthumb -Os \-fno-exceptions -fno-rtti -fno-threadsafe-statics \-fno-use-cxa-atexit -fno-unwind-tables \-std=c++17 -Wall -Werror
Industry Standards
MISRA C++ 2023 is the latest edition, updating the original MISRA C++ 2008 to cover C++17. Key restrictions:
| Area | What MISRA C++ 2023 Restricts | Rationale |
|---|---|---|
| Exceptions | Shall not be used | Non-deterministic control flow |
| Dynamic allocation | Shall not be used (or strictly controlled) | Fragmentation, non-deterministic timing |
| RTTI | dynamic_cast and typeid shall not be used | Flash overhead, runtime cost |
| Multiple inheritance | Diamond inheritance prohibited | Complexity, ambiguous dispatch |
goto | Shall not be used | Unstructured control flow |
| Unions | Restricted; prefer std::variant | Type confusion bugs |
AUTOSAR C++14 is the automotive industry standard. It is a superset of MISRA C++ 2008 with rules tailored for C++14 features. AUTOSAR explicitly permits templates, constexpr, auto type deduction, and range-based for loops -- recognizing that modern C++ features improve safety when used correctly.
Joint Strike Fighter (JSF) AV C++ is an older standard from Lockheed Martin, historically influential but largely superseded by MISRA C++ 2023 and AUTOSAR C++14.
C++ Standard Choice for Embedded
| Standard | Killer Features for Embedded | Adoption |
|---|---|---|
| C++11 | auto, enum class, constexpr, move semantics, static_assert | Legacy baseline |
| C++14 | Relaxed constexpr (loops, local variables), variable templates | AUTOSAR baseline |
| C++17 | constexpr if, std::optional, std::string_view, std::variant, structured bindings | Rapidly growing |
| C++20 | Concepts, consteval, std::span, ranges | Early adoption |
| C++23 | std::expected, constexpr containers | Experimental |
C++17 is the current sweet spot for new embedded projects: it gives you constexpr if for zero-cost compile-time branching, std::optional and std::variant for safe value types, and std::string_view for zero-copy string handling -- all without heap allocation.
Debugging Story: The Exception That Wasn't
A robotics team porting a desktop library to a Cortex-M7 MCU spent two days tracking down a hard fault that appeared only under load. The library compiled cleanly with -fno-exceptions, but deep in a template instantiation, a std::vector::at() call was still compiled. With exceptions disabled, the out-of-bounds .at() call was silently converted to std::abort(), which on their bare-metal system triggered a hard fault with no diagnostic output.
The fix was two-fold: replace std::vector with std::array (fixed-size, no heap), and replace .at() with a custom bounds-checking wrapper that logged the index and array size to a fault register before halting. The lesson: -fno-exceptions does not magically remove all exception-related code paths -- it turns throw into abort(), which is even harder to debug without a fault handler.
Lesson: When disabling exceptions, audit every call to .at(), std::visit (with missing variant alternatives), and any other function that throws. Replace them with explicit bounds checks that produce actionable diagnostics.
What interviewers want to hear: You know the concrete costs of exceptions (flash overhead from unwinding tables, non-deterministic latency) and RTTI (per-class metadata in flash), not just that "they are expensive." You can list which STL components are heap-free and safe for embedded use. You understand extern "C" prevents name mangling and is required for C/C++ interop, and you know the #ifdef __cplusplus guard pattern. You can rattle off the key compiler flags (-fno-exceptions, -fno-rtti, -fno-threadsafe-statics) and explain what each one removes. You are aware of MISRA C++ 2023 and AUTOSAR C++14 as the governing standards for safety-critical embedded C++.
Interview Focus
Classic Interview Questions
Q1: "Why are exceptions disabled in embedded C++?"
Model Answer Starter: "Exception handling requires unwinding tables that add 10-30% to flash size -- on a 64 KB MCU, that is 6-19 KB lost to a feature you rarely use. Stack unwinding during a throw also has non-deterministic latency because the time depends on call depth, which violates hard real-time guarantees. With -fno-exceptions, any throw becomes std::abort(), so I use error codes or std::expected for error propagation instead."
Q2: "Which STL containers can you safely use in embedded C++?"
Model Answer Starter: "std::array is the primary container -- it is a zero-overhead wrapper around a C array with bounds checking via .at() and compatibility with <algorithm>. std::optional stores a nullable value inline with no heap. std::string_view is a non-owning, zero-copy view of a string. std::bitset, std::pair, and std::tuple are also heap-free. I avoid std::vector, std::string, std::map, and <iostream> because they allocate from the heap. For algorithms, <algorithm> and <numeric> operate on iterators and are safe with any container, including std::array."
Q3: "How does extern "C" work and why is it needed in embedded projects?"
Model Answer Starter: "C++ mangles function names to encode parameter types for overload resolution. A function read(uint8_t) might become _Z4readh in the symbol table. C does not mangle names -- it stays as read. When C++ code calls a C function, the linker searches for the mangled name and fails. extern "C" tells the C++ compiler to use C linkage -- no mangling -- so the linker finds the correct symbol. In headers shared between C and C++, I wrap declarations in #ifdef __cplusplus extern "C" { #endif ... #ifdef __cplusplus } #endif so the header compiles in both languages."
Q4: "What is placement new and when would you use it in embedded?"
Model Answer Starter: "Placement new constructs an object at a specific memory address without allocating from the heap. You provide a pre-allocated buffer and new (buffer) Type(args) calls the constructor in that buffer. In embedded, I use it to initialize objects in a statically allocated byte array, in shared memory regions, or in memory pools. The critical thing to remember is that there is no matching delete -- you must call the destructor explicitly with obj->~Type() when the object's lifetime ends."
Q5: "What does MISRA C++ restrict and why?"
Model Answer Starter: "MISRA C++ 2023 bans exceptions, dynamic allocation, RTTI, goto, and diamond multiple inheritance. The common thread is determinism and analyzability: exceptions create non-deterministic control flow, dynamic allocation introduces fragmentation risk and non-deterministic timing, RTTI adds hidden flash cost, and diamond inheritance creates ambiguous dispatch. MISRA is mandatory for automotive (ISO 26262), medical (IEC 62304), and aerospace (DO-178C) software. AUTOSAR C++14 extends MISRA with automotive-specific rules and explicitly permits modern C++ features like templates, constexpr, and auto when used safely."
Trap Alerts
- Don't say: "C++ is too heavy for embedded" -- that reveals unfamiliarity with zero-cost abstractions. The correct position is that C++ is safe for embedded when you disable the expensive features.
- Don't forget: The
#ifdef __cplusplusguard aroundextern "C"in shared headers. Without it, a C compiler will reject the file. - Don't ignore:
std::arrayas the default container. If you say "I use raw C arrays in C++," the interviewer will question whether you understand modern C++ practice.
Follow-up Questions
- "How would you implement a custom allocator for
std::vectorthat uses a static memory pool?" - "What happens if you call
dynamic_castwith-fno-rttienabled?" - "How does CRTP replace virtual functions in embedded C++?"
- "What is the difference between
constexpr ifand a preprocessor#iffor platform-specific code?" - "How do you handle
std::visiton astd::variantwhen exceptions are disabled?"
For hands-on Q&A practice covering C/C++ concepts, pointers, memory, and embedded patterns, see the C/C++ Concepts Interview Q&A page.
Practice
❓ Why are C++ exceptions typically disabled in embedded systems?
❓ Which of these STL components allocates from the heap and should be avoided in embedded C++?
❓ What does extern "C" prevent the C++ compiler from doing?
❓ What does the -fno-rtti compiler flag disable?
❓ In a header shared between C and C++, how should extern C be guarded?
Real-World Tie-In
Automotive Body Control Module -- A Tier 1 supplier migrated a body control module from C to C++14 under AUTOSAR C++14 guidelines. They used std::array for fixed-size sensor buffers, enum class for state machine states (eliminating three bugs caused by implicit integer conversion in the C version), and RAII lock guards for RTOS mutexes. The final binary was 2% smaller than the C version because templates enabled dead-code elimination that the C preprocessor macros had blocked.
Industrial Motor Controller -- A motor drive firmware compiled with -fno-exceptions -fno-rtti -fno-threadsafe-statics ran on a Cortex-M4 with 256 KB flash. The team used std::optional<FaultCode> to replace null-pointer error signaling, catching two latent null-dereference bugs during the port. std::string_view replaced const char* in the diagnostic UART output, eliminating three buffer-overrun risks without adding any heap usage.
Medical Infusion Pump -- A Class III medical device used a strict subset of C++17 validated against MISRA C++ 2023. The team deleted global operator new so any accidental heap allocation was a compile error. All interprocess communication used placement new into statically allocated shared-memory buffers. The FDA reviewer noted that the compile-time enforcement of no-heap policy simplified the memory safety argument in the 510(k) submission.
