В практической библиотеке ресурс должен освобождаться функцией close, а не delete. Как связать такой ресурс с RAII в C++?
Для ресурса, который освобождается не через delete, используют std::unique_ptr с пользовательским deleter. Он вызывается при уничтожении владельца, reset или передаче другого ресурса и выполняет нужную операцию освобождения, например close или fclose.
Такой unique_ptr выражает эксклюзивное владение ресурсом, но тип указателя включает тип deleter: std::unique_ptr<T, D>. Deleter должен корректно освобождать именно этот ресурс и не должен выбрасывать исключения из деструктора.
Ресурсы в системных и библиотечных API часто представлены дескрипторами или указателями на объекты C-библиотеки. Их нельзя безопасно освобождать универсальным delete: для каждого типа ресурса предусмотрена собственная функция, например std::fclose для FILE*.
Ручной вызов функции освобождения требует следить за каждым выходом из области видимости, включая ранний return и исключения. RAII переносит эту ответственность в деструктор объекта-владельца и тем самым связывает время жизни управляющего объекта с временем владения ресурсом.
Если ресурс, открытый через std::fopen, передать обычному std::unique_ptr<FILE>, его стандартный deleter попытается применить delete, что не соответствует правилам владения этим ресурсом и приводит к неопределённому поведению.
Если оставить ресурс сырым указателем, нужно вручную гарантировать вызов std::fclose на всех путях выхода. Ошибка в одном из путей вызывает утечку ресурса, а повторное освобождение или освобождение чужого ресурса приводит к другой ошибке управления памятью.
Пользовательский deleter задаёт операцию освобождения. Упрощённо unique_ptr хранит указатель и объект deleter; когда unique_ptr владеет ненулевым указателем, его деструктор вызывает deleter для этого указателя.
Если std::fopen завершился неудачно и вернул nullptr, владельцу нечего освобождать. При успешном открытии FilePtr вызовет std::fclose при уничтожении, в том числе если функция завершится исключением.
Важные ограничения:
unique_ptr, поэтому std::unique_ptr<FILE> и FilePtr — разные типы.unique_ptr может увеличиться из-за состояния deleter; для пустого deleter реализация часто оптимизирует хранение, но полагаться на конкретный размер не следует.release() отключает автоматическое освобождение и передаёт сырой дескриптор вызывающему коду, поэтому после него ответственность снова становится ручной.Для совместно разделяемого владения возможен std::shared_ptr с пользовательским deleter, но это не делает ресурс автоматически совместно используемым по смыслу. Такой вариант добавляет контрольный блок и счётчик ссылок, поэтому для единственного владельца обычно предпочтителен unique_ptr.
Модуль журналирования открывает файл через std::fopen, а затем выполняет несколько операций, каждая из которых может завершиться ошибкой. Рассматривались три варианта: ручной вызов std::fclose в каждом месте выхода, отдельный класс-обёртка и std::unique_ptr с deleter.
Ручное освобождение имело минимальные накладные расходы, но легко ломалось при добавлении нового раннего выхода. Отдельный класс лучше выражал намерение и позволял добавить операции над файлом, однако требовал писать и поддерживать собственный интерфейс.
Выбран unique_ptr с std::fclose: он уже предоставляет необходимую семантику эксклюзивного владения, корректно работает при исключениях и не требует отдельного класса. В результате код открытия файла возвращает объект-владелец, а вызывающий код не должен помнить о конкретной функции освобождения.
Почему нельзя просто использовать std::unique_ptr<FILE> для ресурса, полученного через std::fopen?
Такой тип по умолчанию использует std::default_delete<FILE>, то есть предполагает освобождение через delete. Объект, созданный fopen, не был создан выражением new, а правила C-библиотеки требуют вызова fclose. Пользовательский deleter не является косметической настройкой: он определяет корректный протокол завершения владения ресурсом.
Что происходит с пользовательским deleter при перемещении unique_ptr?
При перемещении владение указателем переходит в новый unique_ptr, а исходный становится пустым. Состояние deleter также переносится или копируется в соответствии с требованиями и правилами перемещения конкретного типа deleter. Это важно для stateful deleter: после перемещения освобождение должно выполняться с тем состоянием, которое действительно нужно для данного ресурса.
Может ли такой unique_ptr безопасно управлять заимствованным, а не принадлежащим ресурсу?
Нет, если он будет уничтожен обычным образом: deleter выполнит освобождение, хотя объект или дескриптор принадлежит другому коду. Для заимствованного ресурса следует использовать сырой указатель или ссылку без передачи владения либо отдельный тип наблюдателя. release() может технически убрать автоматическое освобождение, но это ручная операция с риском утечки и потому не заменяет корректное разделение владения в интерфейсе.