7.02 std::exception Subclassing, Object Slicing Pitfalls & ROM Bloat in Embedded Firmware
💻 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
📚 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
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_frameDWARF 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:
#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.
catch (std::exception e)) invokes the copy constructor of the base class, discarding (slicing off) all derived class members and overriding behavior.
__cxa_throw requires extensive DWARF stack-unwinding tables and dynamically allocates memory for the exception payload, posing severe ROM/RAM bloat risks.
[[nodiscard]] enforces that callers inspect the returned error status, preventing silent failures and unhandled error states at compile time.
-fno-exceptions flag tells the compiler not to generate exception frames or unwinding support, drastically shrinking code size.