Что происходит со временем жизни объекта после явного вызова его деструктора, если память под него не освобождена?
После явного вызова деструктора время жизни объекта заканчивается, но занимающее его хранилище остаётся доступным. Использовать это место как прежний объект нельзя, пока в нём не будет корректно создан новый объект; повторный вызов деструктора для уже уничтоженного объекта является ошибкой.
Явное уничтожение объекта не освобождает память автоматически. Освобождение хранилища и завершение времени жизни объекта — разные операции.
Раздельное управление временем жизни объекта и памятью появилось из-за низкоуровневых задач C++, где память выделяется заранее, а объекты создаются и уничтожаются выборочно. Это необходимо для аллокаторов, контейнеров, размещения объектов в общей памяти, реализации union и типов вроде std::optional.
Обычный RAII скрывает эту сложность: конструктор начинает время жизни объекта, а деструктор автоматически вызывается при выходе из области видимости. Явное управление требуется только в инфраструктурном коде, где стандартный срок жизни объекта недостаточно гибок.
После вызова деструктора указатель может по-прежнему содержать тот же адрес, но это уже не означает наличие объекта по этому адресу. Разыменование такого указателя как указателя на прежний объект нарушает правила времени жизни объектов.
Если память была получена через new, явный вызов деструктора не заменяет delete: нужно отдельно завершить время жизни объекта и отдельно освободить хранилище. Обратная ошибка тоже опасна: вызов delete сам вызывает деструктор, поэтому предварительный явный вызов обычно приводит к повторному уничтожению.
Для объекта классового типа время жизни завершается при начале выполнения его деструктора. Байтовое хранилище при этом не исчезает: оно может оставаться частью массива байтов, буфера аллокатора или блока памяти, выделенного оператором new.
В том же хранилище можно создать новый объект с помощью размещающего создания. Новый объект должен помещаться в хранилище, иметь подходящее выравнивание и размер, а его создание не должно пересекаться с ещё живым объектом.
В простом случае новый объект того же типа прозрачно заменяет старый, и указатель можно использовать повторно после нового создания. Для некоторых случаев, например потенциально перекрывающихся подобъектов или объектов с особыми правилами размещения, может потребоваться std::launder; это не универсальное средство для легализации доступа к уже уничтоженному объекту.
Главный практический компромисс — контроль против сложности. Ручное управление позволяет переиспользовать память без повторного выделения, но требует самостоятельно соблюдать выравнивание, порядок разрушения, исключения, правила доступа и соответствие операций выделения и освобождения. Поэтому в прикладном коде предпочтительнее std::optional, стандартные контейнеры и собственные RAII-обёртки.
В реализации высокопроизводительного пула памяти блоки выделяются крупными порциями, а объекты внутри них создаются и уничтожаются независимо. Простое удаление каждого объекта через delete не подходит: память принадлежит пулу и должна освобождаться целиком, а не по одному объекту.
Рассматривались два варианта. Постоянное выделение через new упростило бы управление временем жизни, но добавило бы накладные расходы и фрагментацию. Размещение объектов в заранее выделенном буфере уменьшает эти расходы, однако требует ручного вызова деструкторов и строгого учёта занятых слотов.
Выбран второй вариант: пул создаёт объекты в подходящих слотах, вызывает деструкторы при освобождении слотов, а память возвращает только при уничтожении самого пула. Это решение корректно, потому что время жизни объектов и срок жизни общего хранилища управляются раздельно; на практике такую логику заключают в RAII-компонент, чтобы исключения не оставляли живые объекты без разрушения.
Нет, если между этими событиями не был создан новый объект в том же хранилище. При выходе из области видимости компилятор снова попытается вызвать деструктор, а объект уже уничтожен. Такое повторное разрушение даёт неопределённое поведение, особенно если деструктор освобождает ресурсы.
Безопасная ручная схема должна гарантировать, что автоматический вызов деструктора не произойдёт для уже уничтоженного объекта. Обычно для этого используют необязательное хранилище, например std::optional, либо специализированный тип-обёртку, а не обычную локальную переменную.
Нет. Деструктор освобождает ресурсы, которыми владеет сам объект, например файловый дескриптор или память внутреннего буфера, но не обязан освобождать хранилище, в котором размещён объект. Хранилище может быть частью стека, массива байтов, памяти пула или области, полученной через new.
Если объект создан обычным new, корректная ручная последовательность обычно состоит из вызова деструктора и последующего освобождения сырого хранилища соответствующим механизмом. Для обычного владения эту последовательность должен выполнять RAII-владелец, иначе легко получить утечку или двойное освобождение.
Нет, это зависит от правил прозрачной замены объекта. Для обычного полного объекта того же типа, размещённого точно поверх старого, указатель обычно снова может обозначать новый объект. Но для некоторых подобъектов, объектов с потенциальным перекрытием и отдельных случаев изменения типа автоматическое использование старого указателя не гарантируется.
В таких ситуациях нужно получить корректный указатель на новый объект, иногда применив std::launder, а иногда — изменить структуру кода так, чтобы не сохранять старые указатели и ссылки. std::launder не продлевает время жизни и не создаёт объект: он только помогает корректно получить указатель в случаях, предусмотренных правилами языка.