Я читал кое-что об опасности использования размещения new вместо оператора new[], но не мог понять, нормально ли следующее или нет, а если нет, то что с этим не так [...]
Для operator new[]
по сравнению с новым размещением это действительно плохо (как в типичном аварийном типе неопределенного поведения), если вы смешиваете две стратегии вместе.
Основной выбор, который вам обычно приходится делать, — использовать тот или иной. Если вы используете operator new[]
, то вы заранее конструируете все элементы на всю емкость контейнера и перезаписываете их в методах типа push_back
. Вы не уничтожаете их при удалении в таких методах, как erase
, просто оставляете их там и настраиваете размер, перезаписываете элементы и т.д. Вы создаете и выделяете несколько элементов за один раз с помощью operator new[]
, а также уничтожаете и освобождаете их все за один раз с помощью operator delete[]
.
Почему новое место размещения используется для стандартных контейнеров
Первое, что нужно понять, хотите ли вы начать скручивать свои собственные векторы или другие совместимые со стандартом последовательности (которые не являются просто связанными структурами с одним элементом на узел) таким образом, чтобы фактически уничтожать элементы при их удалении, создавать элементы (а не просто перезаписать их) при добавлении состоит в том, чтобы разделить идею выделения памяти для контейнера и создания элементов для него на месте. Так что, как раз наоборот, в данном случае размещение нового — это неплохо. Это фундаментальная необходимость для достижения общих качеств стандартных контейнеров. Но мы не можем смешивать его с operator new[]
и operator delete[]
в этом контексте.
Например, вы можете выделить память для хранения 100 экземпляров T в reserve
, но вы также не хотите создавать их по умолчанию. Вы хотите сконструировать их в таких методах, как push_back
, insert
, resize
, fill ctor
, range ctor
, copy ctor
и т. д. -- методах, которые на самом деле добавляют элементы, а не только возможность их хранения. Вот почему нам нужно новое placement.
В противном случае мы теряем универсальность std::vector
, которая позволяет избежать построения элементов, которых нет, может копировать конструкцию в push_backs
, а не просто перезаписывать существующие с помощью operator=
и т. д.
Итак, начнем с конструктора:
_data = new T[_capacity];
... это вызовет конструкторы по умолчанию для всех элементов. Мы не хотим этого (ни требования ctor по умолчанию, ни эти расходы), поскольку весь смысл использования placement new
заключается в создании элементов вместо выделенной памяти, а это уже привело бы к созданию всех элементов. В противном случае любое использование размещения нового в любом месте будет пытаться построить уже построенный элемент во второй раз и будет UB.
Вместо этого вы хотите что-то вроде этого:
_data = static_cast<T*>(malloc(_capacity * sizeof(T)));
Это просто дает нам необработанный кусок байтов.
Во-вторых, для push_back
вы делаете:
_data[_size++] = t;
Это попытка использовать оператор присваивания и, после нашей предыдущей модификации, для неинициализированного/недопустимого элемента, который еще не создан. Итак, мы хотим:
new(_data + _size) T(t);
++size;
... что заставляет его использовать конструктор копирования. Это соответствует тому, что на самом деле должен делать push_back
: создавать новые элементы в последовательности, а не просто перезаписывать существующие.
Ваш метод стирания требует некоторой доработки даже на базовом логическом уровне, если вы хотите обрабатывать удаление из середины контейнера. Но только с точки зрения управления ресурсами, если вы используете новое размещение, вы хотите вручную вызывать деструкторы для удаленных элементов. Например:
if (p == &back()) { --_size; return end(); }
... должно быть больше похоже на:
if (p == &back())
{
--size;
(_data + _size)->~T();
return end();
}
Ваш emplace_back
вручную вызывает деструктор, но он не должен этого делать. emplace_back
следует только добавлять, а не удалять (и уничтожать) существующие элементы. Это должно быть очень похоже на push_back
, но просто вызывать команду перемещения.
Ваш деструктор делает это:
~myVector() {
delete[] _data;
}
Но опять же, это UB, когда мы используем этот подход. Мы хотим что-то более похожее на:
~myVector() {
for (int j=0; j < _size; ++j)
(_data + j)->~T();
free(_data);
}
Есть еще много всего, что нужно охватить, например, безопасность исключений, которая представляет собой совершенно другую банку червей.
Но это должно помочь вам начать правильное использование размещения new в структуре данных против некоторого распределителя памяти (malloc/free
в этом примерном случае).
Последний, но тем не менее важный:
(не удалось использовать stl по причинам памяти)
... это может быть необычной причиной. Ваша реализация не обязательно использует меньше памяти, чем vector
с reserve
, вызываемым заранее, чтобы дать ему соответствующее capacity
. Вы можете сбрить несколько байтов на уровне каждого контейнера (не на уровне каждого элемента) с выбором 32-битных интегралов и отсутствием необходимости хранить распределитель, но это будет очень небольшая экономия памяти. в обмен на кучу работы.
Подобные вещи могут быть полезным учебным упражнением, хотя они помогут вам создавать некоторые структуры данных, выходящие за рамки стандарта, более совместимым со стандартом способом (например, развернутые списки, которые я считаю весьма полезными).
В итоге мне пришлось заново изобретать некоторые vectors
и векторные контейнеры по причинам ABI (мы хотели, чтобы контейнер, который мы могли передавать через наш API, гарантированно имел один и тот же ABI независимо от того, какой компилятор использовался для создания плагина). Даже тогда я бы предпочел просто использовать std::vector
.
Обратите внимание, что если вы просто хотите взять под контроль то, как vector
выделяет память, вы можете сделать это, указав свой собственный распределитель с совместимым интерфейсом. Это может быть полезно, например, если вам нужен vector
, который выделяет 128-битную выровненную память для использования с выровненными инструкциями перемещения с использованием SIMD.
person
Community
schedule
25.11.2015
new T[ n ];
не создает элементы; он только выделяет память. - person user2296177   schedule 25.11.2015new
по умолчанию будет создавать каждый элемент. - person Mark Ransom   schedule 25.11.2015