Как стать автором
Обновить

Комментарии 33

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

Ибо у блока выделенной памяти должен быть заголовок с размером и указателем на следующий блок.

Глупости. Никто никому ничего не должен. Посмотрите как работает jemalloc, также очень похожим образом работает slub в линукс-ядре.

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

Слишком приблизительным образом.

Записанный в начале блока размер совсем не обязательно корректно отражает количество элементов в массиве. То есть деление размера блока на размер элемента в общем случае будет больше или равно количеству элементов массива. А нам нужно знать точное количество элементов.

... Это во-первых,

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

Интересно, а почему вообще были введены эти два оператора? Ведь один delete вполне мог бы проверять, что он сейчас будет удалять, массив или не массив.

Желание снизить оверхед для хранения не-массивов?

А как он будет это проверять ? А почему именно так, может я по-другому хочу ?

Вот чтобы была возможность сделать свою реализацию - разрешили перегрузить оба варианта.

Как - это была бы забота компилятора, так же как сейчас

Слишком категорично, не находите?

Если задача не решаема или в заданных условиях решаема не оптимально, то это проблема постановщика задачи, а не компилятора. (Иди туда - не знаю куда, делай то - не знаю что)

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

Тем более, что частота использования "сырых" массивов в прикладных задачах, вместо, хотя бы, std::vector, минимальна.
В геймдеве, например, вообще была бы связка malloc+placement_new(optional), а то просто загрузка всех данных из файла с коррекцией указателей.

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

Имхо это скорее обычная практика С/С++ - перекладывать оверхед на программиста; я просто хотел убедиться, что причина именно в этом.

const char* p =...;

delete p;

Определите там массив или нет

В чем смысл этого примера? Все прекрасно поняли, что автор предыдущего комментария имел в виду предлагаемую реализацию, которая каким-то способом сохраняет признак "массив - не массив" при выделении памяти через new или new []. В такой реализации, разумеется, нет вообще никакой сложности в том, чтобы определить массив это или нет.

Ведь один delete вполне мог бы проверять, что он сейчас будет удалять, массив или не массив.

Поскольку в C++ (как и в C) массив объектов может быть "прозрачно" низведен до указателя на объект, то компилятор, рассчитывая только на информацию о типе, не всегда может гарантированно понять, с чем именно он имеет дело в данном конкретном случае - с массивом объектов или с указателем на один объект, без некоей дополнительной информации, которую надо где-то специально хранить, а это идет вразрез с принятой в плюсах идиомой "you don't pay for what you don't use".

Во-первых, да, избежание оверхеда для единичных объектов.

Во-вторых, delete обязан уметь выполнять полиморфное удаление, которое совершенно не актуально для delete []. То есть это существенно разные функциональности, калит которые в один оператор было бы неправильно.

Ну и как следствие - независимые механизмы перегрузки скрытых за ними операторов выделения сырой памяти.

Также, в третьих, начиная c С++14 реализации имеют право объединять запросы на память между соседними new-expressions, то есть заголовок сырого блока памяти уже не является тривиально доступным из каждого указателя, возвращенного new-expression.

delete обязан уметь выполнять полиморфное удаление, которое совершенно не актуально для delete [].

Разве не актуально? Мне казалось, никакой разницы в этом смысле не должно быть

В смысле? В С++ не существует "полиморфных массивов" и не существует полиморфного delete []. В delete [] не разрешается передавать указатель на базовый класс. В delete [] статический тип удаляемого объекта должен совпадать с его динамическим типом. В противном случае поведение не определено.

Ерунду написали. delete [] такой же полиморфный, как и delete. Он узнает реальный размер через виртуальный деструктор (знает vtbl, значит знает тип) первого элемента в массиве, точно так же, как это делает delete для единичного объекта.

Это "ерунду" написал не я, а авторы языка С++: дедушка Страуструп и WG21 ISO C++ Committee.

Еще раз повторяю "для тех кто в танке", и не для разглагольствования, а вызубривания наизусть: никакого полиморфного delete [] в С++ не существует и никогда не существовало. Попытки применения delete [] для удаления объекта (или массива), чей статический тип не совпадает с динамическим, приводит к неопределенному поведению. Ни с какими "виртуальными деструкторами", "vtbl" и т.п. delete [] никогда не работает.

delete [] не имеет ничего общего с полиморфным поведением delete.

P.S. Это "пионэрское" верование в существование некоего "полиморфного delete []" я встречаю уже не первый раз. Откуда-то это лезет... Где-то у этого "гнездо"... Я помню, что MSVC++ в старинных версиях своего особенного видения С++ пытался "подарить" пользователям "полиморфный delete []", но даже они со времен прекратили заниматься подобной чушью. Тем не менее эта чушь все живет...

Я честно признаюсь, что просто не знал. Спасибо!

И я не думаю, что это откуда-то "лезет", просто это ну.. _совершенно нелогично_ ведь! Почему один delete полиморфный, а другой - нет?..

Плюс я часто вижу пример "коллекция указателей на BaseItem" как объяснение зачем вообще нужен полиморфизм; очень странно, что обычный массив при этом себя ведет иначе.

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

На самом деле все логично, если немного задуматься.

Когда у нас указатель на один объект, у него может быть какой угодно размер, а полиморфость является штатным ожидаемым поведением.

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

Гипотетически конечно можно было разрешить вставлять в массив элементы разных подтипов пока у них совпадает размер, но это ужасное и небезопасное решение, поэтому было принято что тип всех элементов массива должен совпадать. А раз он совпадает, то и деструктор у всех элементов ровно один и лежит по известному адресу, а значит и полиморфность тут не нужна.

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

Я должен признаться, что совершенно упустил это из виду; я почему-то все это время рассуждал (у себя в голове) о массиве указателей, а не о массиве объектов. Но при удалении массива указателей деструкторы для указываемых объектов вообще не вызываются.

Тогда неясно даже, как оказаться в ситуации, в которой нужен полиморфный delete[]? Взять указатель на массив объектов и скастовать его к указателю на массив объектов другого типа, а потом надеяться на полиморфность?

(Между делом хочу все же отметить, что - по моему мнению - практически на каждую уродливую бородавку в С++ есть логичное объяснение, к сожалению, это не делает язык красивым и консистентным, потому что объяснение это ad hoc и порождают не цельное, так сказать, полотно, а лоскутное одеяло. Это не делает объяснение нелогичным, но порождает фрустрацию из-за необходимости запоминать кучу мелочей)

Для сомневающихся - цитата из стандарта С++11:

5.3.5 Delete

...

2 ... In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.

Да абсолютно пофигу. Никогда не пользовался new[] и delete[] с классами, когда есть std::vector. Так что незнание понятно и простительно. А тебя это прям сильно задело.

 А тебя это прям сильно задело.

На "ты" мы вроде не переходили, но окей; меня не задело, с чего бы, я и сам не знал. Просто цитате (с номером пункта) из стандарта лично у меня веры больше, чем комментарию случайного человека.

У std::vector<T> те же самые ограничения что и у T[]. Чтобы использовать полиморфность нужно хранить в массиве не объекты, а указатели на них.

Но, ЧСХ, и MSVC до сегодняшнего для продолжает поддерживать свой "полиморфный delete []", и GCC тоже полез в это болото. В GCC delete [] с некоторых пор ведет себя по-майкрософтовски.

Формально никто им этого, разумеется, не запрещает, ибо поведение все равно не определено.

Из Большой Тройки только в clang не поддерживается "полиморфный delete [] ".

То есть неудивительно, что среди программистов "от сохи" может встречаться верование, что delete [] может использоваться полиморфно.

Допускаю, но я лично не на MSVC "воспитан"

Потому что можно запросить блок памяти 160 байт и разместить там один объект размером 16 байт. И как delete узнает, что там 1 объект, а не 10? delete[] подсказывает - всё, что выделено, занято объектами, которые нужно удалить.

Мне как сочувствующему (не пишу ни на C, ни на C++) было очень познавательно. Спасибо за статью

но вместо внятного объяснения – просто прикрываются магическим "undefined behavior"

Это кто и где так говорит, интересно? Это же стандартнейший вопрос по С++ на собеседовании. Стыдно не знать.

Тут стоит заметить, что во всех популярных реализациях дополнительное хранение размера массива в new[]/delete[] используется только в двух случаях:

  • Элемент массива является классом с нетривиальным деструктором. Размер нужен для вызова правильного количества деструкторов.

  • Элемент массива является классом с перегруженным operator delete [](void *ptr, std::size_t size).Размер нужен для вычисления правильного значения аргумента для параметраsize.

В остальных случаях дополнительного хранения размера массива вnew[]/delete[]не производится, т.е. эти операторы выделяют память так же, как голый malloc.

Компилятор MSVC++ до последнего времени содержал баг - игнорировал вторую причину из перечисленных выше, в результате чего в нем при вызове перегруженного operator delete [] в качестве размера передавалось "мусорное" значение. Надо проверить, возможно уже исправили...

  1. Не нужно

    Ни new ни delete не нужно использовать, перегрузки new подсвечивать красным на анализе, использование new/delete оранжевым new[]/delete[] ярко красным

а зачем он нужен этот "сырой" массив если есть, например, std контейнеры ?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий