В чём опасность хранения массива, созданного через new , в std::unique ptr вместо std::unique ptr?

В чём опасность хранения массива, созданного через new[], в std::unique_ptr<T> вместо std::unique_ptr<T[]>?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Это приводит к неопределённому поведению при уничтожении: std::unique_ptr<T> использует delete, тогда как память, выделенная через new[], должна освобождаться через delete[]. Для массива нужен std::unique_ptr<T[]> либо другой владелец с корректным способом освобождения.

Исторический контекст

В C++ существуют парные операции управления динамической памятью: new/delete — для одного объекта и new[]/delete[] — для массива. Такое разделение связано с тем, что массив требует корректного уничтожения всех элементов и использует отдельные правила освобождения памяти.

RAII переносит эту ответственность из ручного кода в деструктор объекта-владельца. Специализации std::unique_ptr позволяют описать правильную операцию освобождения непосредственно типом владельца.

Постановка проблемы

Если указатель, полученный от new[], передать в std::unique_ptr<T>, его деструктор вызовет delete. Это несоответствие между способом выделения и освобождения памяти является неопределённым поведением: программа может повредить память, некорректно вызвать деструктор элемента или проявить ошибку только в другой сборке.

Проблема особенно опасна тем, что ошибка обычно не видна в месте создания указателя. Она проявляется позже — при выходе владельца из области видимости, исключении или явном сбросе ресурса.

Подробное решение

Для массива следует использовать std::unique_ptr<T[]>:

#include <memory> struct Item { ~Item() {} }; int main() { auto items = std::make_unique<Item[]>(3); items[0] = Item{}; }

В этом случае деструктор владельца применяет delete[], поэтому уничтожаются элементы массива и корректно освобождается выделенная память. Специализация для массива также предоставляет оператор обращения по индексу.

std::unique_ptr<T[]> не хранит размер массива. Поэтому он защищает от неправильного delete, но не проверяет границы индекса; размер нужно хранить отдельно или выбрать контейнер с размером, например std::vector<T>.

Использование std::unique_ptr<T> допустимо для одного объекта, созданного через обычный new. Нельзя полагаться на то, что одинаковый тип элемента T автоматически означает совместимость с массивным способом выделения.

Альтернативой может быть unique_ptr с пользовательским удалителем, который вызывает delete[], но для обычного динамического массива это усложняет тип и интерфейс без преимуществ. Обычно предпочтительнее std::vector, если нужны размер, владение массивом и безопасная работа с диапазоном.

Ситуация из практики

Компонент получает число элементов во время выполнения и должен владеть непрерывным буфером объектов. Разработчик выбирает std::unique_ptr<Item> и передаёт ему результат new Item[count]. В рабочей сборке ошибка долго не проявляется, но после добавления нетривиального деструктора Item программа начинает аварийно завершаться при освобождении буфера.

Вариант с сырым указателем и ручным delete[] требует контролировать все пути выхода, включая исключения, поэтому он наиболее уязвим. Вариант со std::unique_ptr<Item[]> автоматически освобождает ресурс, но размер всё равно нужно хранить отдельно.

Если компоненту нужны размер и стандартные операции над диапазоном, выбирается std::vector<Item>. Если требуется именно отдельный динамический массив с минимальной дополнительной семантикой, выбирается std::unique_ptr<Item[]> вместе с сохранённым размером. Это устраняет несоответствие delete/delete[] и сохраняет автоматическое освобождение при исключениях.

Что кандидаты часто упускают

  1. Достаточно ли заменить delete на delete[] вручную в деструкторе класса-владельца?

Нет, такая замена решает только конкретный путь уничтожения. Нужно также корректно реализовать копирование, перемещение, присваивание и обработку исключений; иначе остаются двойное освобождение, утечки или владение одним буфером из нескольких объектов. std::unique_ptr<T[]> уже задаёт некопируемое уникальное владение и корректно освобождает ресурс при уничтожении.

  1. Почему std::unique_ptr<T[]> не гарантирует безопасность обращения по индексу?

Потому что он хранит указатель, но не размер массива. Выражение с индексом не сравнивается с границами во время выполнения, поэтому неверный индекс всё ещё может привести к неопределённому поведению. Для проверки размера и интеграции с алгоритмами обычно лучше использовать std::vector или передавать пару указатель-размер либо std::span с гарантированным временем жизни буфера.

  1. Можно ли передать std::unique_ptr<T[]> функции, которая принимает std::unique_ptr<T>?

Нет, это разные специализации с разной семантикой освобождения, и безопасного преобразования между ними нет. Такое ограничение предотвращает передачу массивного ресурса владельцу, который вызовет delete вместо delete[]. Интерфейс функции должен явно принимать std::unique_ptr<T[]> либо, если функция не принимает владение, использовать невладеющее представление с отдельно известным размером.