Избыточность оператора delete[] в С++

95
23 августа 2019, 10:00

Тут я как всегда не вовремя задумался вот над каким вопросом. При выделении памяти из кучи очевидно, что в куче должна сохраняться информация о размере запрошенной области памяти и о количестве запрошенных элементов. Зачем же тогда существуют отдельно операторы delete и delete[]? Ведь независимо от того, запросили мы вектор или один элемент, в куче есть информация и о размере запрошенной области, и о количестве запрошенных элементов. Не может быть, чтобы при запросе одного элемента в куче не сохранялась информация о том, что запрошен один элемент. А если это так, то оператор delete вполне может разобраться (по служебной информации, содержащейся в куче) был ли запрошен массив или был запрошен один элемент. И, соответственно, вернуть в кучу память или одного элемента, или вектора. Получается, что оператор delete[] избыточен.

UPD1:

В нынешнем подходе С++ поступает более экономично: хранит свою дополнительную информацию только в массивах объектов с нетривиальным деструктором.

То есть Вы хотите сказать, что у кучи есть несколько форматов? Один формат для запрошенного одного элемента, другой формат для запрошенного вектора, третий формат для запрошенного одного элемента с нетривиальным аллокатором/деструктором, четвертый формат для запрошенного вектора с нетривиальным аллокатором/деструктором? Ну, это дело конечно разработчиков компилятора и кучи, как там они видят свою задачу чтобы сделать ее максимально эффективной. Но на первый взгляд иметь много форматов кучи это не так чтобы однозначно было эффективнее, чем хранить всю информацию и в рантайме разбираться, что же именно там сейчас лежит. Тем более, что разбираться в рантайме и при подходе со многими форматами кучи все равно придется.

UPD2:

И да, нетривиальный аллокатор тоже хранит в куче информацию о количестве запрошенных элементов и о размере одного элемента. Имея эту информацию нетривиальный деструктор может разобраться, что именно ему надо удалять. И опять же в этом случае не нужен оператор delete[], достаточно оператора delete.

UPD3:

Оставим пока в покое нетривиальные аллокаторы. Рассмотрим пока что стандартные аллокаторы, тем более что проблемы в обоих случаях одинаковы.

Итак, есть куча и есть операторы new и new[]. Оба оператора обязаны занести в служебную информацию кучи данные о размере одного объекта и о КОЛИЧЕСТВЕ ОБЪЕКТОВ в запросе. Соответственно, оператор возврата памяти delete нужен только один, так как по служебной информации рантайм может и должен разобраться сколько именно объектов было запрошено. Соответственно, оператор delete[] избыточен.

Теперь рассмотрим нестандартные (пользовательские) аллокаторы. Совершенно так же пользовательские new и new[] обязаны занести в служебную информацию кучи данные о размере одного объекта и о КОЛИЧЕСТВЕ ОБЪЕКТОВ в запросе. Дополнительно пользовательские new и new[] обязаны занести в служебную информацию кучи указатель на пользовательский деструктор. Опять же в этом случае оператор возврата памяти delete нужен только один, так как по служебной информации рантайм может и должен разобраться сколько именно объектов было запрошено. Соответственно, оператор delete[] избыточен.

Answer 1

Во-первых, даже если в куче и сохраняется информация о размере запрошенного блока в байтах, способ хранения этой информации может быть известен delete только в том случае, если используется "штатный" аллокатор. Но процесс выделения "сырой" памяти в С++ является перегружаемым пользователем. Как только выделение памяти перешло на пользовательский аллокатор, delete уже не может определить размер блока.

Во-вторых, даже если размер блока в байтах известен, по этому размеру все равно нельзя однозначно восстановить точное количество элементов в массиве, чтобы вызвать точное количество деструкторов. Размер блока может превышать точное значение, требуемое для хранения элементов.

В-третьих, не ясно, о каком "количестве запрошенных элементов" вы говорите. Количество запрошенных элементов сохраняет именно new [] и вычитывает именно delete []. Для того, собственно, delete [] и сделан отдельным от delete. Смотрите детали здесь: Откуда C/C++ знает сколько надо освободить памяти, если не знает размер массива?

Теоретически можно сделать "умный delete", который сам всегда во всем разбирается. Но это приведет к безусловной необходимости хранить дополнительную информацию во всех блоках памяти. В нынешнем подходе С++ поступает более экономично: хранит свою дополнительную информацию только в массивах объектов с нетривиальным деструктором.

Фактически почти такой "умный delete" у вас уже есть. Никто вам не запрещает везде просто безусловно пользоваться new[]/delete[] и забыть про существование new/delete. То есть одиночные объекты просто выделять как массивы размера 1. Но это будет несколько более расточительно (и не поддерживает полиморфного удаления).

Отвечая на ваш UPD1:

В типичной реализации у блока в С++ куче фактически три формата: для одиночного объекта (new), для массива с тривиальными деструкторами (new[]) и для массива с нетривиальными деструкторами (new[]).

При этом первые два формата можно было бы считать совпадающими с точки зрения внутренней структуры, т.к. это просто "блоки памяти". Но тут вмешивается тот факт, что механизмы выделения/освобождения "сырой" памяти в С++ являются перегружаемыми пользователем: независимо для new/delete и для new[]/delete[]. Поэтому это - отдельные форматы.

Ответ на ваш UPD3:

Я не знаю, с чего вы взяли, что "Оба оператора обязаны занести в служебную информацию кучи данные о размере одного объекта и о КОЛИЧЕСТВЕ ОБЪЕКТОВ в запросе". Это совершенно не так.

Еще раз: просто new такой информации НЕ сохраняет. И new[] для типов с тривиальным деструктором никакой информации о количестве или размере объектов НЕ хранит тоже. Эти форматы просто выделяют память через обычный malloc, освобождают через обычный free и никакой дополнительной внутренней информации в этом блоке памяти не сохраняют. С точки зрения С++ памяти требуется ровно столько, сколько нужно для хранения пользовательских данных.

Особняком стоит только new[] для массива с нетривиальными деструкторами. Только он сохраняет в блоке служебную информацию о точном количестве элементов в массиве (и поэтому выделяет несколько больше памяти, чем требуется для пользовательских данных).

Информация о размере одного элемента в таком блоке не хранится вообще никогда - это низачем не нужно.

Я при этом говорю только о стандартных аллокаторах. Пользовательские аллокаторы тут ни при чем.

Answer 2

Вы смешиваете в одну кучу интерфейс и детали реализации. И, вероятнее всего, забываете, что С++ используется не только на x86-совмесимых системах.

Язык (интерфейс) ничего не знает о куче. Это реализация. New вполне может выделять память, например, из slab-аллокатора, в то время как New[] будет использовать кучу.

К тому же на микроконтроллерах есть серьезное ограничение по памяти, и делать одинаковую реализацию New/Delete и New[]/Delete[] просто расточительно, т.к. вторая пара должна знать не только размер участка памяти, но и количество реальных элементов в нем.

Answer 3

Дело в том что хотя рантайм и знает размер(что в общем случае не верно, он может размер и не хранить), он не знает что там содержится. Теоретически можно попытаться угадать, но тогда алгоритм угадывания придется описать в спецификации. Это ненужное усложнение.

Краткий ответ: чтобы не усложнять спецификацию и оставить ее более гибкой.

Answer 4

Насколько мне известно, существование new / delete и new[] / delete[] обосновано девизом: не платим за то, что не используем.

Такой подход позволяет экономить память и такты на лишних проверках.

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

В C такой проблемы разделения нет, потому что free() просто освобождает память, не вызывая деструкторы.

Если же мы попытаемся удалить массив объектов C++, используя только информацию о размере блока памяти, то мы не сможем этого сделать. Нам не хватает информации.

Поэтому, new[] заводит для каждого массива объектов минимум два счетчика. Содержимое этих счетчиков зависит от реализации компилятора, но обычно в счетчиках хранятся размер блока памяти и тип объекта. Зная тип объекта - мы имеем информацию о размере объекта и его деструкторе.

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

Если бы существовал только delete, который умел бы работать и с одиночными объектами, и с массивами объектов, тогда при работе с одиночными объектами происходил бы перерасход памяти и тактов процессора на лишние проверки.

Подтверждением этой теории служит тот факт, что следующий код:

while (1)
{
    float *arr = new float[1000];
    delete arr;
    arr = new float;
    delete[] arr;
}

В большинстве ситуацией не приводит к падениям или утечкам. Да, это UB, но я видел множество случаев такого кода, который был написан умышленно и работал годами.

Поскольку float не имеет деструктора, то и проблем при перепутывании delete и delete[] чаще всего не возникает.

Конечно, внутри все это устроено намного сложнее, но общая суть происходящего примерно такая.

READ ALSO
Какие библиотеки нужны для реализации Android приложения, которое воспроизводит аудио-файлы?

Какие библиотеки нужны для реализации Android приложения, которое воспроизводит аудио-файлы?

Нужна помощь в определении необходимых библиотек, чтобы реализовать приложение в котором будут воспроизводиться музыкальные файлы и различные...

119
Убрать пробелы в начале и конце строки (editext)

Убрать пробелы в начале и конце строки (editext)

Цель: в edittext убрать пробелы в начале и конце строки

95
Заполнение json динамически

Заполнение json динамически

У меня модель пользователя, в которую следует добавить поле json, значения которого будут разными, то есть не знаю как создать его

87