Рассмотрите создание владельца из сырого указателя. Предположим, что после успешного new выделение control block для std::shared_ptr завершается исключением. Кто освободит объект?
#include <memory>
struct Deleter {
void operator()(int* p) const noexcept {
delete p;
}
};
void create() {
std::shared_ptr<int> value(new int(42), Deleter{});
}
Объект не будет потерян: если конструктор std::shared_ptr не сможет создать управляющий блок после успешного new, он вызовет переданный deleter для сырого указателя. В данном примере будет выполнен delete p.
Это гарантирует базовую исключительную безопасность именно конструктора std::shared_ptr: либо создаётся владелец, либо переданный ресурс освобождается.
До появления RAII владение динамической памятью часто разделялось между несколькими участками кода: один выполнял new, другой должен был не забыть delete. Исключения сделали такую схему особенно опасной: управление могло покинуть функцию до освобождения ресурса.
Умные указатели перенесли ответственность за освобождение ресурса в объект-владелец. Для std::shared_ptr этого недостаточно только после успешного создания управляющего блока — поэтому его конструкторы также определяют поведение при сбое собственного создания.
Выражение new int(42) сначала выделяет память и создаёт объект. Затем конструктор std::shared_ptr должен сохранить указатель, deleter и служебную информацию в управляющем блоке.
Если второй этап выбросит исключение, например из-за невозможности выделить память, обычный код после конструктора не выполнится. Без специальной гарантии объект, созданный через new, остался бы без владельца, что привело бы к утечке памяти.
Для конструктора std::shared_ptr с сырым указателем и пользовательским deleter предусмотрено исключительное поведение: если создание владельца не завершилось успешно, переданный deleter вызывается для исходного указателя. В примере это эквивалентно выполнению delete для int.
Минимальная демонстрация самого принципа:
После успешного конструирования deleter сохраняется в управляющем блоке и будет вызван при уничтожении последнего std::shared_ptr. Если же создание управляющего блока завершится исключением, deleter используется немедленно для очистки new int(7).
Это не означает, что любой способ передачи сырого указателя безопасен. До самого вызова конструктора выражение new уже создало ресурс, поэтому нельзя сначала сохранить указатель в отдельной переменной, а затем выполнять между new и созданием владельца операции, способные выбросить исключение, без собственного механизма очистки.
Также deleter должен соответствовать способу получения ресурса. Для памяти из new нужен delete, для массива — delete[], а для внешнего ресурса — соответствующая функция освобождения. Кроме того, исключения из пользовательского deleter недопустимы в нормальной схеме освобождения: обычно его объявляют noexcept, поскольку исключение из уничтожения владельца может привести к завершению программы.
Допустим, библиотечный API возвращает дескриптор, который нужно закрывать функцией close_handle. Вариант с std::shared_ptr<Handle> и пользовательским deleter централизует освобождение и сохраняет совместное владение.
Можно было бы вручную вызвать close_handle в каждом пути выхода, но это усложняет код и повышает риск утечки при исключениях. Можно использовать std::unique_ptr, если владелец только один: это дешевле и яснее выражает владение. Если ресурс действительно разделяется между несколькими компонентами, выбирают std::shared_ptr, принимая стоимость управляющего блока и подсчёта ссылок.
Решение с пользовательским deleter оправдано только при корректном протоколе владения. Важно передавать указатель в shared_ptr ровно один раз и не создавать для того же ресурса второй независимый управляющий блок.
1. Вызывается ли пользовательский deleter, если исключение произошло при создании управляющего блока?
Да. Если сырой указатель уже передан конструктору std::shared_ptr, а создание владельца не завершилось, для очистки используется указанный deleter. Для стандартного конструктора без пользовательского deleter применяется delete.
Это относится к исключению внутри создания shared_ptr, а не к произвольным операциям, выполненным до вызова его конструктора.
2. Можно ли безопасно создать два shared_ptr из одного сырого указателя?
Нет. Например, std::shared_ptr<int> a(raw); std::shared_ptr<int> b(raw); создают два независимых управляющих блока. Каждый считает себя единственным владельцем и в итоге оба попытаются освободить один ресурс.
Правильный подход — создать один shared_ptr, а затем копировать именно его. Если объект уже управляется shared_ptr, нельзя восстанавливать владельца через get().
3. Почему std::make_shared часто предпочтительнее конструкции с new?
std::make_shared<T> обычно выполняет одно совместное выделение памяти для объекта и управляющего блока, поэтому уменьшает число аллокаций и исключает промежуточный сырой указатель в пользовательском коде. При исключении во время создания объекта стандартная библиотека сама корректно очищает уже выделенные части.
Однако отдельный shared_ptr с пользовательским deleter необходим, когда объект уже получен из внешнего API или должен освобождаться нестандартным способом. Поэтому make_shared не заменяет пользовательское управление ресурсами во всех случаях.