11.01 std::unique_ptr, Ownership Transfer (std::move), and Custom Deleters for Hardware Registers
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
π 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_ptrallocates 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/STREXon 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:
#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.
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.
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.
std::move() transfers ownership from p1 to p2, resetting p1 to nullptr.