The requirement stems from C++'s type decay rules and the necessity of compile-time deleter selection. When an array type is passed to a template, it decays to a pointer, stripping the array extent information that would distinguish scalar (delete) from array (delete[]) deallocation. std::unique_ptr resolves this through partial template specialization: the primary template std::unique_ptr<T> uses std::default_delete<T> invoking scalar delete, while std::unique_ptr<T[]> instantiates std::default_delete<T[]> which invokes delete[]. This explicit syntax ensures the compiler generates the correct destruction code without runtime type introspection or overhead.
Context: A low-latency audio processing engine receives PCM sample buffers from a hardware driver API that returns float* allocated via new float[buffer_size]. These buffers must pass through a chain of digital signal processing filters while maintaining strict real-time constraints and exception safety.
Problem: The team required a smart pointer solution that provided RAII safety for these C-style arrays without introducing std::vector's size/capacity tracking overhead, which would violate cache-line alignment requirements for SIMD operations. Critically, using scalar delete on array-allocated memory would corrupt the heap and crash the audio pipeline.
Raw pointer with manual deletion. This approach utilized naked float* pointers with explicit delete[] calls in every exit path. Pros: Zero abstraction overhead and direct hardware API compatibility. Cons: Exception-unsafe; if a filter threw during processing, the buffer leaked, and maintaining correct deletion logic across twenty different filter stages became unmaintainable. Rejected due to reliability risks in production.
std::vector<float> container. Wrapping buffers in std::vector provided automatic memory management and size tracking. Pros: Exception safety and bounds checking availability. Cons: std::vector implicitly stores capacity pointers (typically 24 bytes overhead), which broke the fixed-size DMA alignment contracts with the audio hardware. Additionally, std::vector assumes mutable ownership and potential reallocation, conflicting with the driver's fixed buffer pool.
std::unique_ptr<float[]> specialization. This solution employed std::unique_ptr<float[]> which instantiates std::default_delete<float[]> automatically. Pros: Zero overhead (sizeof equals one pointer), guaranteed delete[] invocation, movable semantics for efficient filter chain handoffs, and compile-time prevention of copying. Cons: Loses runtime size information requiring parallel tracking, and std::make_unique<float[]>(size) value-initializes elements which may be unnecessary for POD types.
Decision and Result. We selected std::unique_ptr<float[]> combined with a lightweight span-like view for size tracking. This provided exception safety without violating hardware alignment constraints. The system processed audio streams for months without memory leaks, and the explicit array specialization caught a critical bug during compilation where a developer attempted std::unique_ptr<float> with array-new, forcing the correct syntax before runtime.
Why does std::unique_ptr<Base[]> reject initialization from new Derived[N] when std::unique_ptr<Derived> converts to std::unique_ptr<Base>?
Array types exhibit non-covariant behavior unlike single pointers. While Derived* implicitly converts to Base* via pointer adjustment, Derived[] cannot convert to Base[] because array indexing arithmetic depends on the static type size; accessing element i in a Base[] view of Derived[] would compute incorrect byte offsets. Therefore, std::unique_ptr's array specialization explicitly deletes converting constructors between different array types to prevent accessing misaligned memory, whereas the scalar version permits the conversion (requiring virtual destructors for safety).
How does std::make_unique<T[]>(n) initialize elements compared to std::make_unique<T>(args...), and why does this limit its applicability?
The array overload std::make_unique<T[]>(n) performs value-initialization on all n elements, which zero-initializes scalars or default-constructs objects. This differs from the scalar form which forwards arguments to T's constructor. This distinction prevents using std::make_unique for arrays of non-default-constructible types, as you cannot pass constructor arguments for individual elements. Candidates often attempt std::make_unique<NonDefaultConstructible[]>(5, args), which fails to compile, forcing either manual loops or std::vector usage with emplacement.
What undefined behavior manifests when std::unique_ptr<T> (scalar) manages memory from new T[N], and why do compilers remain silent?
The scalar std::unique_ptr utilizes std::default_delete<T>, which calls delete (scalar delete). When applied to array-allocated memory from new T[N], this constitutes a mismatch resulting in undefined behavior—typically freeing only the first element's memory or corrupting the heap allocator's metadata. Compilers do not warn because the template parameter T decays; new T[N] returns T*, and the type system loses the array distinction at the point of std::unique_ptr construction. This silent failure mode is precisely why std::unique_ptr<T[]> exists as a distinct type-safe alternative.