Project 7.02 Section 7 ⚡ Embedded Relevance: High std::exception what() Object Slicing Code Bloat enum class AUTOSAR C++14

7.02 std::exception Subclassing, Object Slicing Pitfalls & ROM Bloat in Embedded Firmware

Executive Summary: Building domain-specific exception hierarchies by inheriting from std::runtime_error and std::exception. We analyze how exception unwinding tables (.eh_frame / DWARF) add 15-40KB of Flash overhead, why dynamic exception allocation is hazardous in constrained SRAM, and how to refactor with zero-cost tagged error codes.

💻 1. Annotated Source Code

#include <iostream>
#include "AngryCatException.h"
using namespace std;

void feedKitty(int numTreats);

int main() {
	int numTreats = 0;

	cout << "How many treats do you want to feed kitty?" << endl;
	cin >> numTreats;

	try {
		feedKitty(numTreats);
	}
	catch (const AngryCatException& err) {
		cout << err.what() << endl;
	}

	return 0;
}

void feedKitty(int numTreats) {
	if (numTreats < 3) {
		throw AngryCatException();
	}
	else if (numTreats < 6) {
		throw AngryCatException("I'm still not happy!");  //not angry, just disappointed
	}

	cout << "Kitty is happy with " << numTreats << " treats." << endl;
}
#ifndef ANGRY_CAT_EXCEPTION_H
#define ANGRY_CAT_EXCEPTION_H

#include <stdexcept>
#include <string>
using namespace std;

class AngryCatException : public runtime_error {
	public:
		AngryCatException() : runtime_error("Made kitty angry!") {
		}

		AngryCatException(const string& err) : runtime_error(err) {
		}
};

#endif

📐 2. Architecture & UML Class Model

📐 Custom Exception Class Hierarchy & DWARF .eh_frame Model
+ Public - Private # Protected
<<class>> std::runtime_error Base Exception
(none / stateless)
+runtime_error(what_arg: const string&)
+what() : const char*[virtual, noexcept]
<<class>> CustomException Custom Domain Exception
-errorCode : int32_t
+CustomException(msg: string, code: int = -1)
+getErrorCode() : int32_t const
+what() : const char*[override, noexcept]
🔗 Architectural Relationships & Hierarchy
CustomException ──▷ public inherits ──▷ std::runtime_error

📚 3. Core C++ Concepts Deep-Dive

1. Creating Custom Domain Exceptions

In standard C++, custom exception classes inherit from std::runtime_error (for runtime issues) or std::logic_error (for precondition violations), overriding the virtual const char* what() const noexcept method.

📐 Exception Inheritance Hierarchy UML

<<std-base>> std::exception
+ what() : const char* [virtual]
+ ~exception() [virtual]
<<std-runtime>> std::runtime_error
+ runtime_error(msg: string)
+ what() : const char* [override]
<<custom-domain>> AngryCatException
+ AngryCatException()
+ AngryCatException(msg: string)
+ what() : const char* [override]

2. Object Slicing in Catch Handlers

Always catch exceptions by const reference (catch (const std::exception& e)). Catching by value (catch (std::exception e)) slices derived member variables away and destroys polymorphic what() dispatch.

⚡ 4. Embedded Systems & Hardware Reality

1. The 15KB - 40KB Flash Penalty of C++ Exceptions

Enabling C++ exceptions (-fexceptions) forces GCC/Clang to emit:

  • .eh_frame DWARF Unwind Tables: Metadata mapping instruction addresses to catch blocks.
  • __cxa_throw & __cxa_allocate_exception: Runtime support code that dynamically allocates memory on an internal emergency heap when throwing.

On a 32KB or 64KB Flash microcontroller (e.g. STM32F103, ATmega328P), exception runtime support can consume over 50% of total available ROM.

2. Non-Deterministic Stack Unwinding in Real-Time ISRs

When an exception is thrown, the runtime traverses stack frames looking for a matching handler. The time required depends on call depth and active local objects, destroying deterministic hard real-time latency guarantees.

🚫 AUTOSAR Rule A15-0-1 & MISRA C++:2023 Rule 18.0.1

Exceptions shall not be used in safety-critical systems unless an upper bound on execution time can be strictly proven. In bare-metal and RTOS firmware, compile with -fno-exceptions.

💡 5. Production-Ready Embedded Refactoring

Here is how modern embedded C++ implements zero-overhead, strongly typed error reporting without heap allocation or unwind tables:

💡 Production-Ready Refactor
#include <cstdint>
#include <string_view>

// 1. Strongly-typed 1-byte error code (Zero RAM overhead)
enum class [[nodiscard]] SensorStatus : uint8_t {
    Ok = 0,
    Timeout,
    BusCollision,
    ChecksumMismatch,
    CriticalThresholdExceeded
};

// 2. Pure compile-time string converter (Stored in Flash ROM .rodata)
constexpr std::string_view to_string(SensorStatus status) noexcept {
    switch (status) {
        case SensorStatus::Ok:                        return "OK";
        case SensorStatus::Timeout:                   return "I2C Bus Timeout";
        case SensorStatus::BusCollision:              return "Arbitration Lost";
        case SensorStatus::ChecksumMismatch:          return "CRC8 Checksum Failure";
        case SensorStatus::CriticalThresholdExceeded: return "Critical Temperature Threshold Exceeded";
    }
    return "Unknown Error";
}

// 3. Deterministic return with [[nodiscard]] preventing unhandled errors
[[nodiscard]] SensorStatus readTemperature(int16_t& out_temp_celsius) noexcept {
    // Hardware reading logic...
    if (/* hardware timeout */ false) {
        return SensorStatus::Timeout;
    }
    out_temp_celsius = 42;
    return SensorStatus::Ok;
}

📝 Knowledge Verification Quiz

Test your understanding of the C++ concepts and embedded microcontroller trade-offs covered in this guide. Click any option for instant feedback.

Q1. What is 'object slicing' when handling C++ exceptions?
A Catching a derived exception class by value instead of const reference, copying only the base class slice and losing derived data
B Allocating exceptions on the heap with new
C Dividing an exception object into multiple threads
D Splitting an exception across two catch blocks
Detailed Explanation: Catching by value (catch (std::exception e)) invokes the copy constructor of the base class, discarding (slicing off) all derived class members and overriding behavior.
Q2. Why is the C++ exception runtime support library (__cxa_throw) hazardous on small microcontrollers?
A It adds 15KB-40KB of Flash code bloat and relies on an internal heap allocation for exception objects
B It permanently disables CPU interrupts
C It alters the crystal oscillator clock frequency
D It prevents C++ classes from having member functions
Detailed Explanation: __cxa_throw requires extensive DWARF stack-unwinding tables and dynamically allocates memory for the exception payload, posing severe ROM/RAM bloat risks.
Q3. What does the C++ [[nodiscard]] attribute accomplish when applied to an error-returning function or enum?
A Causes the compiler to emit a warning/error if the caller ignores the returned status code
B Automatically throws a std::runtime_error on failure
C Forces the return value into the CPU cache line
D Allocates the return value in static BSS memory
Detailed Explanation: [[nodiscard]] enforces that callers inspect the returned error status, preventing silent failures and unhandled error states at compile time.
Q4. Which compiler flag disables C++ exception generation and stack unwinding tables entirely?
A -fno-exceptions
B -O2 -g
C -fno-rtti
D -nostdlib
Detailed Explanation: The -fno-exceptions flag tells the compiler not to generate exception frames or unwinding support, drastically shrinking code size.