Search topics...
C/C++ Programming & Low-Level Foundations
intermediate
Weight: 4/10

C++ Embedded Constraints & STL

Navigate C++ in resource-constrained embedded systems — disabled exceptions and RTTI, heap-free programming, safe STL subsets, compiler flags, C/C++ interop with extern C, and industry guidelines (MISRA C++, AUTOSAR C++14).

c++
embedded
constraints
stl
misra
no-exceptions
Loading quiz status...

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_cast and typeid are unavailable.
  • Heap-free programming: new/delete are 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

AspectEmbedded C++ Practice
ExceptionsDisabled (-fno-exceptions); use error codes or std::expected
RTTIDisabled (-fno-rtti); use static_cast and compile-time polymorphism
Heap allocationForbidden or tightly controlled; static allocation preferred
STL containersstd::array, std::optional, std::bitset only; no vector, map, string
C interopextern "C" on all shared headers; guard with #ifdef __cplusplus
Standard versionC++14 (AUTOSAR baseline), C++17 (constexpr if), C++20 (concepts)
Coding standardsMISRA 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 class prevents 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:

CostDetail
Flash overheadUnwinding tables add 10-30% to binary size -- unacceptable on a 64 KB MCU
Non-deterministic latencyStack unwinding time depends on call depth; violates hard real-time guarantees
Heap dependencySome exception implementations allocate on throw
No partial adoptionOne 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)

cpp
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.

💡Interview Tip: Know the C++23 Connection

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_cast is unavailable -- use static_cast when the type is known at compile time, or implement a manual type tag (an enum field in the base class).
  • typeid is 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, no delete.
  • 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.
cpp
// 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
⚠️Common Trap: Overriding Global new/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?

ComponentHeap-Free?Embedded Safe?Notes
std::arrayYesSafeFixed-size array with bounds checking via .at()
std::optionalYesSafeNullable value without pointers; no heap
std::string_viewYesSafeNon-owning view of a string; zero-copy
std::bitsetYesSafeFixed-size bit array; replaces manual bitmasks
std::tupleYesSafeFixed-size heterogeneous container
std::pairYesSafeTwo-element tuple
&lt;algorithm&gt;YesSafestd::sort, std::find, std::copy operate on iterators, not containers
&lt;numeric&gt;YesSafestd::accumulate, std::inner_product
std::variantYesSafe (C++17)Type-safe union; replaces raw unions
std::vectorNoAvoidHeap-allocated dynamic array
std::stringNoAvoidHeap-allocated; use std::string_view or fixed char[]
std::map / std::unordered_mapNoAvoidHeap nodes; use sorted std::array + binary search
std::shared_ptrNoAvoidReference count block is heap-allocated
&lt;iostream&gt;NoAvoidPulls 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 &lt;algorithm&gt;. There is no reason to use raw C arrays in C++ embedded code.

std::optional vs null pointers: std::optional&lt;T&gt; 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):

cpp
// In a C++ source file, wrap the C header include
extern "C" {
#include "vendor_hal.h" // C header with C linkage
#include "freertos/task.h"
}

Exposing C++ functions to C callers:

cpp
// In a C++ header, guard the declarations
#ifdef __cplusplus
extern "C" {
#endif
void app_init(void); // Callable from C startup code
void 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++

FlagEffectWhy
-fno-exceptionsDisables exception handlingSaves 10-30% flash, deterministic latency
-fno-rttiDisables runtime type informationSaves flash, disables dynamic_cast/typeid
-fno-threadsafe-staticsDisables mutex guards on local static initializationAvoids pulling in pthread on bare-metal; safe if you init statics before threading starts
-fno-use-cxa-atexitDisables __cxa_atexit for static destructor registrationStatic objects on MCUs are never destroyed; saves code and RAM
-fno-unwind-tablesRemoves .eh_frame unwind tablesFurther reduces flash after disabling exceptions
-nostdlibDoes not link standard C/C++ librariesFull control over startup code and memory layout

A typical embedded C++ compile line:

text
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:

AreaWhat MISRA C++ 2023 RestrictsRationale
ExceptionsShall not be usedNon-deterministic control flow
Dynamic allocationShall not be used (or strictly controlled)Fragmentation, non-deterministic timing
RTTIdynamic_cast and typeid shall not be usedFlash overhead, runtime cost
Multiple inheritanceDiamond inheritance prohibitedComplexity, ambiguous dispatch
gotoShall not be usedUnstructured control flow
UnionsRestricted; prefer std::variantType 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

StandardKiller Features for EmbeddedAdoption
C++11auto, enum class, constexpr, move semantics, static_assertLegacy baseline
C++14Relaxed constexpr (loops, local variables), variable templatesAUTOSAR baseline
C++17constexpr if, std::optional, std::string_view, std::variant, structured bindingsRapidly growing
C++20Concepts, consteval, std::span, rangesEarly adoption
C++23std::expected, constexpr containersExperimental

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 __cplusplus guard around extern "C" in shared headers. Without it, a C compiler will reject the file.
  • Don't ignore: std::array as 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::vector that uses a static memory pool?"
  • "What happens if you call dynamic_cast with -fno-rtti enabled?"
  • "How does CRTP replace virtual functions in embedded C++?"
  • "What is the difference between constexpr if and a preprocessor #if for platform-specific code?"
  • "How do you handle std::visit on a std::variant when exceptions are disabled?"
💡Practice More C/C++ Interview Questions

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&lt;FaultCode&gt; 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.

Was this helpful?