Отчего размер std::unique_ptr зависит от типа deleter и какую роль здесь играет оптимизация пустого базового класса?
std::unique_ptr хранит не только указатель, но и объект удаления — deleter. Если deleter пустой и не содержит состояния, реализация обычно применяет оптимизацию пустого базового класса и не выделяет ему отдельное место; если deleter хранит данные, размер указателя обычно увеличивается.
Точный размер стандарт не фиксирует: он зависит от реализации, типа указателя и deleter. Поэтому нельзя переносить предположение о размере std::unique_ptr между платформами без проверки.
std::unique_ptr появился в C++11 как безопасная RAII-замена ручному управлению ресурсами через new и delete. В отличие от std::shared_ptr, он не обязан хранить счётчик ссылок, поэтому обычно имеет минимальные накладные расходы.
Однако освобождать ресурс нужно не всегда через обычный delete. Файловые дескрипторы, дескрипторы ОС, память из специального аллокатора и объекты массивов требуют собственного способа удаления. Для этого unique_ptr параметризуется типом deleter.
Владелец ресурса может находиться внутри большого контейнера или передаваться через границы производительного кода. Если deleter содержит состояние, каждый элемент std::unique_ptr становится крупнее, что увеличивает потребление памяти и может ухудшить локальность данных.
Ошибочно считать, что любой unique_ptr<T, D> занимает размер одного обычного указателя. Кроме того, использование указателя на функцию в качестве deleter часто добавляет отдельное машинное слово, даже если сама функция не имеет состояния.
Концептуально std::unique_ptr<T, D> хранит пару из указателя и объекта типа D. Для пустого deleter, например безсостоя́тельного функционального объекта, отдельное поле обычно не требуется: объект D размещают как пустую базовую часть или используют эквивалентное сжатое представление.
Здесь deleter — пустой тип: лямбда не захватывает данные. Реализация обычно сжимает его, но полагаться на конкретное значение sizeof(File) нельзя. Если deleter хранит, например, идентификатор аллокатора, этот идентификатор должен присутствовать в каждом объекте указателя.
Тип deleter является частью типа unique_ptr. Поэтому std::unique_ptr<T, D1> и std::unique_ptr<T, D2> — разные типы, даже если оба deleter фактически вызывают одну функцию. Это влияет на совместимость контейнеров, сигнатуры функций и возможность перемещения между типами.
Указатель удаления может быть не обычным T*: стандарт допускает нестандартный тип указателя, заданный через deleter_traits<D>::pointer. Это нужно, например, для некоторых представлений дескрипторов или смещённых указателей и тоже может изменить размер объекта.
Для unique_ptr<T[]> применяется логика удаления массива через delete[]. Передача состояния deleter поддерживает гибкость, но увеличивает размер и усложняет перемещение. На практике для компактного владельца предпочтителен безсостоя́тельный функциональный объект, если ему не нужны параметры.
Сервис хранит миллионы объектов-обёрток над системными ресурсами. В первом варианте разработчик использует std::unique_ptr<Resource, void (*)(Resource*)>. Такой deleter удобен для передачи функции, но указатель на функцию обычно хранится внутри каждого владельца, поэтому элементы контейнера становятся крупнее.
Рассматривались два варианта. Обычный delete прост, но неприменим к ресурсу, полученному из системного API. Указатель на функцию универсален и понятен, но обычно требует дополнительного поля. Безсостоя́тельный тип deleter немного менее гибок, зато позволяет реализации сжать его.
Выбран собственный безсостоя́тельный функциональный объект, потому что способ освобождения фиксирован для данного типа ресурса. Это уменьшило размер владельца на целевой реализации и сохранило автоматическое освобождение при исключениях. Фактический эффект проверили через sizeof на поддерживаемых ABI, не превращая это значение в переносимое требование стандарта.
Гарантирует ли стандарт, что unique_ptr с пустым deleter имеет размер обычного указателя?
Нет, такой общей гарантии нет. Оптимизация пустого базового класса — распространённый способ реализации, но стандарт задаёт семантику владения, а не конкретную компоновку объекта. Размер также может зависеть от нестандартного типа указателя и ABI.
Почему указатель на функцию и безсостоя́тельный функциональный объект дают разный размер?
Указатель на функцию — это значение, которое нужно хранить, чтобы знать, какую функцию вызвать. Безсостоя́тельный функциональный объект не содержит данных; его тип определяет поведение, поэтому реализация обычно может не выделять ему отдельное хранилище. Это оптимизация представления, а не изменение логики deleter.
Что происходит с состоянием deleter при перемещении unique_ptr?
При перемещении передаётся и право владения ресурсом, и состояние deleter в соответствии с правилами перемещения или копирования этого типа. После перемещения исходный unique_ptr обычно становится пустым, но его deleter остаётся корректным объектом в допустимом состоянии. Поэтому deleter должен удовлетворять требованиям, предъявляемым соответствующей операцией unique_ptr; наличие состояния может влиять на возможность и стоимость перемещения.