Почему std::make unique безопаснее прямой передачи результата new в функцию с несколькими аргументами?

Почему std::make_unique безопаснее прямой передачи результата new в функцию с несколькими аргументами?

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

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

std::make_unique сразу связывает созданный объект с std::unique_ptr, поэтому между выделением памяти и установлением владения нет доступного для исключения промежутка. При передаче необёрнутого результата new в функцию исключение при вычислении другого аргумента может оставить объект без владельца и вызвать утечку.

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

Ручное управление памятью требует отдельно создать объект, определить владельца и гарантировать вызов delete на всех путях выхода. Особенно трудно это обеспечить при исключениях, когда обычный линейный порядок выполнения прерывается.

RAII решает исходную проблему тем, что владение ресурсом представляется объектом с деструктором. std::make_unique объединяет выделение памяти, создание объекта и передачу владения так, чтобы ресурс с самого начала находился под контролем RAII-объекта.

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

Рассмотрим функцию, принимающую несколько аргументов. Если один аргумент создаётся через необёрнутый new, а вычисление другого аргумента выбрасывает исключение, управление может не дойти до места, где созданный объект должен быть передан владельцу.

Последствие — утечка памяти. Даже если конкретная форма записи с явным созданием unique_ptr безопасна в данной версии стандарта и при данном deleter, она сложнее для анализа и легче становится ошибочной после изменения интерфейса или пользовательского deleter.

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

std::make_unique<T>(аргументы) выполняет создание объекта и возвращает уже готовый std::unique_ptr<T>. Если конструктор объекта выбрасывает исключение, память корректно освобождается механизмом выделения; если создание успешно, временный smart pointer немедленно становится владельцем ресурса.

Минимальный пример:

#include <memory> struct Widget { Widget(int); }; void consume(std::unique_ptr<Widget>, int); int calculate(); void run() { consume(std::make_unique<Widget>(42), calculate()); }

Если calculate() выбросит исключение, временный std::unique_ptr, уже созданный make_unique, будет уничтожен при раскрутке стека и освободит Widget. Владелец не представлен сырым указателем ни на одном наблюдаемом этапе.

Важно не утверждать, что всякая запись с std::unique_ptr<T>(new T(...)) автоматически небезопасна. В простой отдельной инициализации она обычно корректна, однако make_unique делает границу владения явной и уменьшает зависимость от порядка вычисления аргументов, исключений и особенностей deleter.

Есть и практические ограничения. make_unique не продлевает жизнь объектов, переданных конструктору по сырым указателям, и не делает сам объект потокобезопасным. Для массивов применяется форма std::make_unique<T[]>; для ресурсов, освобождаемых не через delete, нужен подходящий deleter и соответствующая конструкция владения.

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

Сервис создаёт объект конфигурации и передаёт его вместе с результатом загрузки метаданных. В старом варианте объект сначала создавался через new, а затем передавался как сырой указатель; при исключении загрузки метаданных память утекала.

Возможные варианты:

  • оставить сырой указатель и добавить ручной try/catch — работает, но усложняет код и легко ломается при добавлении новых путей выхода;
  • создать unique_ptr отдельно через явную конструкцию — безопаснее, но требует дисциплины и аккуратного выбора deleter;
  • использовать make_unique и передавать владение перемещением или по значению — компактно выражает ответственность за ресурс.

Выбран последний вариант. Объект сразу получает владельца, исключения автоматически запускают уничтожение локального unique_ptr, а сигнатура функции явно показывает передачу владения. В результате исчезла утечка на аварийном пути и не потребовался ручной код очистки.

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

1. Всегда ли выражение std::unique_ptr<T>(new T(...)) приводит к утечке при исключении другого аргумента?

Нет. Нельзя делать безусловный вывод, что любая такая запись небезопасна: значение имеет версия стандарта, порядок вычисления и момент, когда временный unique_ptr уже создан. Однако прямой new концептуально сначала создаёт ресурс без владельца, поэтому такая форма хуже масштабируется и менее явно выражает безопасную границу владения. make_unique предпочтительнее как стандартный способ атомарно получить объект под управлением unique_ptr.

2. Что произойдёт, если конструктор объекта, вызываемый внутри make_unique, выбросит исключение?

Объект считается не созданным, а выделенная для него память освобождается. Возвращаемый unique_ptr не появляется, поэтому владение не нужно вручную передавать или отзывать. Это один из ключевых эффектов RAII: частично выполненное создание не оставляет ресурс без ответственного владельца.

3. Почему make_unique не заменяет передачу сырого указателя в невладеющий интерфейс?

Потому что make_unique создаёт владельца, но не меняет контракт функции. Если функция только временно использует объект и не должна владеть им, ей следует передавать ссылку или сырой указатель как невладеющее представление, при этом владелец должен жить дольше вызова. Передача unique_ptr по значению означает передачу владения, а передача по ссылке — иное соглашение и не должна ошибочно интерпретироваться как передача ресурса.