Требование возникает из правил распада типов C++ и необходимости выбора разрушителей на этапе компиляции. Когда массив передается в шаблон, он распадается на указатель, теряя информацию о размере массива, которая позволила бы отличить скалярное (delete) от массивного (delete[]) освобождения памяти. std::unique_ptr решает эту задачу через частичную специализацию шаблонов: основной шаблон std::unique_ptr<T> использует std::default_delete<T>, вызывая скалярное delete, тогда как std::unique_ptr<T[]> создает экземпляр std::default_delete<T[]>, который вызывает delete[]. Эта явная синтаксис обеспечивает правильную генерацию кода разрушения компилятором без интроспекции типов или накладных расходов во время выполнения.
Контекст: Двигатель обработки аудио с низкой задержкой получает буферы PCM с образцами от API драйвера аппаратного обеспечения, который возвращает float*, выделенные с помощью new float[buffer_size]. Эти буферы должны проходить через цепочку цифровых фильтров сигналов, соблюдая строгие ограничения реального времени и безопасность исключений.
Проблема: Команда требовала решения с использованием умного указателя, который обеспечил бы безопасность RAII для этих массивов в стиле C, не вводя накладные расходы отслеживания размера/емкости от std::vector, что нарушило бы требования выравнивания кэш-линий для SIMD операций. Критически важно, что использование скалярного delete на выделенной памяти массива испортило бы кучу и вызвало сбой в аудиопотоке.
Сырой указатель с ручным удалением. Этот подход использовал голые указатели float* с явными вызовами delete[] на каждом выходном пути. Плюсы: нулевая накладная нагрузка и прямая совместимость с API аппаратного обеспечения. Минусы: небезопасно в отношении исключений; если фильтр выдал исключение во время обработки, буфер утекал, и поддержание правильной логики удаления на протяжении двадцати различных этапов фильтрации становилось неприемлемо. Отклонено из-за рисков надежности в производстве.
Контейнер std::vector<float>. Обертывание буферов в std::vector обеспечивало автоматическое управление памятью и отслеживание размера. Плюсы: безопасность исключений и доступность проверки границ. Минусы: std::vector неявно хранит указатели емкости (обычно 24 байта накладных расходов), что нарушало контракты фиксированного размера выравнивания DMA с аудиоустройством. Кроме того, std::vector предполагает изменяемое владение и потенциальную перераспределяемость, что противоречит фиксированному пулу буферов драйвера.
Специализация std::unique_ptr<float[]>. Это решение использовало std::unique_ptr<float[]>, который автоматически создает экземпляр std::default_delete<float[]>. Плюсы: нулевая накладная нагрузка (размер равен одному указателю), гарантированный вызов delete[], семантика перемещения для эффективной передачи цепочки фильтров и предотвращение копирования на этапе компиляции. Минусы: теряет информацию о размере во время выполнения, требуя параллельного отслеживания, а std::make_unique<float[]>(size) инициализирует элементы по значению, что может быть ненужно для типов POD.
Решение и результат. Мы выбрали std::unique_ptr<float[]> в сочетании с легковесным представлением, подобным span, для отслеживания размера. Это обеспечило безопасность исключений, не нарушая ограничения выравнивания аппаратного обеспечения. Система обрабатывала аудиопотоки в течение месяцев без утечек памяти, и явная специализация массива поймала критическую ошибку на стадии компиляции, когда разработчик пытался использовать std::unique_ptr<float> с массивным new, заставляя использовать правильный синтаксис до выполнения.
Почему std::unique_ptr<Base[]> отвергает инициализацию от new Derived[N], когда std::unique_ptr<Derived> преобразуется в std::unique_ptr<Base>?
Массивные типы демонстрируют нековариантное поведение в отличие от одиночных указателей. Хотя Derived* неявно преобразуется в Base* через корректировку указателя, Derived[] не может преобразоваться в Base[] из-за того, что арифметика индексирования массива зависит от статического размера типа; доступ к элементу i в представлении Base[] массива Derived[] даст неправильные смещения байтов. Следовательно, специализация массива std::unique_ptr явно удаляет конструкторы преобразования между различными массивами, чтобы предотвратить доступ к неправильно выровненной памяти, тогда как скалярная версия допускает преобразование (требуя виртуальные деструкторы для безопасности).
Как std::make_unique<T[]>(n) инициализирует элементы по сравнению с std::make_unique<T>(args...), и почему это ограничивает его применимость?
Специальная версия массива std::make_unique<T[]>(n) выполняет инициализацию значением для всех n элементов, что обнуляет скаляры или выполняет конструкторы по умолчанию объектов. Это отличается от скалярной формы, которая передает аргументы конструктору T. Это различие препятствует использованию std::make_unique для массивов типов, не конструируемых по умолчанию, так как вы не можете передать аргументы конструктора для отдельных элементов. Кандидаты часто пытаются использовать std::make_unique<NonDefaultConstructible[]>(5, args), что не компилируется, заставляя либо использовать ручные циклы, либо обращаться к std::vector.
Какое неопределенное поведение проявляется, когда std::unique_ptr<T (скаляр) управляет памятью от new T[N], и почему компиляторы остаются молчаливыми?
Скалярный std::unique_ptr использует std::default_delete<T>, который вызывает delete (скалярное удаление). При применении к памяти, выделенной массивом через new T[N], это представляет собой несоответствие, приводящее к неопределенному поведению — обычно освобождая лишь память первого элемента или повреждая метаданные распределителя кучи. Компиляторы не предупреждают, потому что параметр шаблона T распадается; new T[N] возвращает T*, и типовая система теряет различие массива на этапе конструктора std::unique_ptr. Этот молчаливый режим отказа и является причиной существования std::unique_ptr<T[]> как отдельного, безопасного по типу альтернативного варианта.