Project 11.01 Section 11 ⚑ Embedded Relevance: Critical std::unique_ptr std::make_unique Move Semantics Custom Deleters MMIO AUTOSAR A18-5-8

11.01 std::unique_ptr, Ownership Transfer (std::move), and Custom Deleters for Hardware Registers

Executive Summary: Deep dive into deterministic memory ownership via std::unique_ptr. Explores move semantics, zero-overhead memory guarantees, why std::shared_ptr is avoided in microcontrollers due to control block RAM overhead and atomic ref-counting, and how custom deleters enable RAII over hardware peripherals.

πŸ’» 1. Annotated Source Code

#include <iostream>
#include <memory>
#include <utility>
using namespace std;

int main() {
	const int ARR_SIZE = 5;

	//unique_ptr<double> myDubPtr(new double);
	unique_ptr<double> myDubPtr = make_unique<double>();
	//auto myArray = make_unique<int[]>(ARR_SIZE);

	*myDubPtr = 3.14;
	cout << "Pointer value: " << *myDubPtr << endl;

	unique_ptr<double> otherPtr = move(myDubPtr);
	cout << "otherPtr: " << *otherPtr << endl;

	//for (int i = 0; i < ARR_SIZE; i++) {
	//	myArray[i] = i * 2;
	//}

	//for (int i = 0; i < ARR_SIZE; i++) {
	//	cout << myArray[i] << endl;
	//}

	return 0;
}

πŸ“ 2. Architecture & UML Class Model

πŸ“ std::unique_ptr Exclusive Ownership & Custom RAII Deleter Model
+ Public - Private # Protected
<<template class>> std::unique_ptr<T, Deleter> Zero-Overhead Smart Pointer
-_M_ptr : T* (Single 4-byte pointer)
+unique_ptr(ptr: T*)
+~unique_ptr()[invokes Deleter]
+operator->() : T*
+operator*() : T&
+release() : T*
+reset(p: T* = nullptr) : void
<<struct>> SpiPeripheral Hardware MMIO Device
+SPI_CR1 : volatile uint32_t*
+SPI_DR : volatile uint32_t*
+writeByte(b: uint8_t) : void
<<struct (functor)>> SpiDeleter Hardware Safe-Shutdown
(none / stateless)
+operator()(spi: SpiPeripheral*) : void const[Gates clock off]
πŸ”— Architectural Relationships & Hierarchy
std::unique_ptr<T, Deleter> ◆── owns exclusive ◆── SpiPeripheral
std::unique_ptr<T, Deleter> ─ ─ > executes on scope exit ─ ─ > SpiDeleter

πŸ“š 3. Core C++ Concepts Deep-Dive

1. Exclusive Ownership & Zero-Cost Abstraction

std::unique_ptr<T> represents exclusive ownership of a resource. Unlike raw pointers, it automatically calls delete when exiting scope (RAII). Because std::unique_ptr stores only the raw pointer (with default deleter), it has zero memory overhead compared to a raw C pointer (sizeof(unique_ptr<T>) == sizeof(T*)).

2. Move Semantics (Ownership Transfer)

Because ownership must be exclusive, std::unique_ptr has its copy constructor deleted. Transferring ownership requires explicit move semantics via std::move(), which resets the source pointer to nullptr.

⚑ 4. Embedded Systems & Hardware Reality

1. Why std::shared_ptr is Often Banned in Bare-Metal Systems

  • RAM Overhead: std::shared_ptr allocates a 16-to-24-byte control block on the heap containing two reference counters, a custom deleter, and an allocator pointer. On a 16KB SRAM microcontroller, this memory bloat is unacceptable.
  • Thread-Safety Atomic Latency: Incrementing and decrementing reference counters requires atomic instructions (e.g., LDREX/STREX on ARM Cortex-M), which disable compiler optimizations and increase instruction cycles.

2. Custom Deleters for Hardware Peripherals (RAII for MMIO)

std::unique_ptr can manage non-heap hardware resourcesβ€”such as hardware mutexes, DMA channels, or power railsβ€”by supplying a custom deleter that turns off clocks or releases locks automatically on scope exit.

⚠️ AUTOSAR C++14 Rule A18-5-8

Objects shall not be created using raw new or delete. All dynamic allocation (if permitted during bootup) must be immediately wrapped in std::unique_ptr or std::make_unique.

πŸ’‘ 5. Production-Ready Embedded Refactoring

Here is how embedded engineers use std::unique_ptr with a custom lambda deleter to automatically power-down an SPI peripheral when done:

πŸ’‘ Production-Ready Refactor
#include <memory>
#include <iostream>

struct SpiPeripheral {
    void write(uint8_t data) { std::cout << "SPI Tx: " << int(data) << '\n'; }
};

// Custom deleter that disables the hardware clock
struct SpiDeleter {
    void operator()(SpiPeripheral* spi) const {
        std::cout << "Hardware Clock Disabled (Safe RAII State)\n";
        // e.g., RCC->APB2ENR &= ~SPI1_EN;
    }
};

using SpiHandle = std::unique_ptr<SpiPeripheral, SpiDeleter>;

void transmitSensorPacket() {
    SpiPeripheral hwSpi;
    SpiHandle handle(&hwSpi); // Automatically unlocks on function exit!
    handle->write(0xAA);
} // <-- SpiDeleter runs automatically here!

πŸ“ 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 the memory size of a std::unique_ptr<int> with the default deleter on a 32-bit ARM Cortex-M architecture?
A 4 bytes (identical to a raw pointer)
B 8 bytes (pointer + reference count)
C 16 bytes (control block overhead)
D 0 bytes (fully optimized away)
Detailed Explanation: With the default stateless deleter, std::unique_ptr incurs zero space overhead and occupies exactly 4 bytes on a 32-bit CPU, perfectly mirroring a raw pointer while providing RAII safety.
Q2. Why is std::make_unique<T>() preferred over std::unique_ptr<T>(new T()) in modern C++?
A It automatically creates a thread for the pointer.
B It guarantees exception safety and prevents resource leaks during multi-argument sub-expression evaluation.
C It converts the pointer into a shared pointer.
D It stores the object in Flash ROM instead of RAM.
Detailed Explanation: std::make_unique prevents potential memory leaks when initializing multiple parameters in a function call where one constructor might throw before the pointer is assigned.
Q3. What happens to the source pointer after executing: std::unique_ptr<int> p2 = std::move(p1);?
A p1 continues to point to the same address.
B p1 is immediately deleted.
C p1 is set to nullptr, relinquishing ownership.
D A compile-time error occurs.
Detailed Explanation: std::move() transfers ownership from p1 to p2, resetting p1 to nullptr.