Project 7.03 Section 7 ⚡ Embedded Relevance: Core Invariants Constructor Failure Two-Phase Init noexcept RAII

7.03 Constructor Invariant Enforcement vs Two-Phase Initialization in Microcontrollers

Executive Summary: Enforcing domain invariants through constructor validation and member validation methods. We examine the classic C++ dilemma of constructor failure (destructors do not run for incomplete objects) and contrast exception-based validation with deterministic Two-Phase Initialization (init()) and static factory methods.

💻 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

📐 Dog Entity & Out-of-Range Bounds Exception Model
+ Public - Private # Protected
<<class>> Dog Entity
-name : std::string
-breed : std::string
+Dog(name: string, breed: string)
+getName() : std::string const
+getBreed() : std::string const

📚 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() or begin()): 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:

💡 Production-Ready Refactor
#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.

Q1. If a C++ constructor throws an exception before completing, what happens to the class destructor?
A The destructor is NOT called for the throwing object
B The destructor is immediately executed
C The destructor is deferred until program termination
D The destructor runs twice
Detailed Explanation: In C++, an object is only considered fully constructed when its constructor finishes execution without throwing. If an exception occurs inside the constructor, the destructor will not execute.
Q2. Why is the Two-Phase Initialization (init()) pattern widely used in embedded microcontrollers?
A Because hardware peripheral initialization can fail, and microcontrollers often compile with -fno-exceptions
B Because it enables multiple inheritance
C Because C++ constructors cannot accept arguments
D Because it increases compilation speed by 50%
Detailed Explanation: In embedded systems where exceptions are disabled, constructors cannot fail safely. An explicit init() method allows returning a status code if hardware peripherals do not respond.
Q3. What happens if a static global object's constructor attempts to write to peripheral registers before microcontroller clock tree initialization?
A A BusFault or HardFault occurs because peripheral bus clocks have not yet been enabled in Reset_Handler
B The compiler automatically fixes the clock tree
C The data is cached until bootup completes
D The CPU ignores the register writes safely
Detailed Explanation: Attempting to access peripheral registers before enabling their respective APB/AHB bus clocks in the clock distribution tree generates a fatal hardware BusFault.
Q4. How does returning std::optional<T> from a static factory method achieve zero-overhead object creation?
A It uses Return Value Optimization (RVO / copy elision) to construct the object directly into the caller's stack frame without heap allocations
B It allocates memory in the global BSS section
C It uses dynamic heap memory pools
D It converts objects into raw void pointers
Detailed Explanation: Guaranteed copy elision (C++17) ensures that the factory method constructs std::optional<T> directly in the caller's stack location without any heap allocation or copy overhead.