7.03 Constructor Invariant Enforcement vs Two-Phase Initialization in Microcontrollers
💻 1. Annotated Source Code
#include <iostream> #include "Dog.h" using namespace std; int main() { try { Dog myDog("German Shepherd"); Dog yourDog("Golden Retriever"); //Dog theirDog("Poodle"); cout << myDog.getBreed() << endl; cout << yourDog.getBreed() << endl; //cout << theirDog.getBreed() << endl; } catch (const runtime_error& err) { cout << err.what() << endl; } return 0; }
#ifndef DOG_H #define DOG_H #include <string> using namespace std; class Dog { public: Dog(string breed); string getBreed() const; private: string breed; }; #endif
#include "Dog.h" #include <stdexcept> using namespace std; Dog::Dog(string breed) { if (breed != "poodle" && breed != "Poodle") { this->breed = breed; } else { throw runtime_error("Poodle? That's not a real dog!"); } } string Dog::getBreed() const { return breed; }
📐 2. Architecture & UML Class Model
📚 3. Core C++ Concepts Deep-Dive
1. Object Invariants and Encapsulation
An invariant is a condition that must always be true for an object to be in a valid, functional state. Constructors establish invariants, and public mutator methods maintain them.
2. Constructor Failure Mechanics
In standard C++, if a constructor throws an exception, the object is considered never created. Crucially, the class destructor will not run. Any sub-objects already initialized will be destroyed in reverse order of declaration, but raw resource pointers owned directly by the class may leak.
⚡ 4. Embedded Systems & Hardware Reality
1. The Two-Phase Initialization Pattern
In embedded systems compiled with -fno-exceptions, constructors cannot throw to report initialization failures (e.g. peripheral hardware unresponsive, DMA channel busy). Firmware designs use Two-Phase Initialization:
- Phase 1 (Constructor): Lightweight construction; sets member variables to safe default/inert states (no hardware I/O).
- Phase 2 (
init()orbegin()): Configures hardware registers, verifies I2C/SPI ACK, and returns a boolean or status code.
2. Static Factory Methods with std::optional
Modern C++17 provides static factory creation methods that validate parameters and return std::optional<T> by value with zero heap allocation.
💡 5. Production-Ready Embedded Refactoring
Here is how embedded engineers design robust classes with deterministic factory initialization:
#include <cstdint> #include <optional> #include <string_view> class UartDriver { private: uint32_t baud_rate_; uint8_t port_id_; bool is_initialized_{false}; // Private constructor enforces creation through verified factory method constexpr UartDriver(uint8_t port, uint32_t baud) noexcept : baud_rate_(baud), port_id_(port) {} public: // Factory method validating invariants before creating the object static std::optional<UartDriver> create(uint8_t port, uint32_t baud) noexcept { if (port > 3) return std::nullopt; // Microcontroller has only UART 0-3 if (baud < 9600 || baud > 921600) return std::nullopt; // Unsupported baud rate return UartDriver(port, baud); } [[nodiscard]] bool init_hardware() noexcept { // Configure MMIO registers (e.g., USART->BRR = ...)... is_initialized_ = true; return true; } };
📝 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.
init() method allows returning a status code if hardware peripherals do not respond.
std::optional<T> directly in the caller's stack location without any heap allocation or copy overhead.