Какие виды использования используются для «размещения новых»?


411

Кто-нибудь здесь когда-либо использовал "размещение нового" в C ++? Если да, то для чего? Мне кажется, это было бы полезно только на оборудовании с отображением памяти.


14
Это просто информация, которую я искал, чтобы вызывать конструкторы объектов для расширенных выделенных пулов памяти. (Надеясь на эти ключевые слова, будет легче найти кого-то в будущем).
Сайд-шоу Боб

2
Он используется в статье Википедии C ++ 11 в конструкторе объединения.
Здравствуйте, до свидания,

@ Здравствуйте, интересно! В статье, на которую вы ссылаетесь, почему вы не можете просто сделать p = ptи использовать оператор присваивания Pointвместо выполнения new(&p) Point(pt)? Я удивляюсь разнице между ними. Будет ли первая вызывать operator=Point, а вторая - копировать конструктор Point? но мне все еще не очень понятно, почему один лучше другого.
Андрей-Никулае Петре

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

@ Andrei-NiculaePetre На самом деле, я нахожу пример в статье в Википедии довольно плохим, поскольку он просто предполагает, что никакого предшествующего объекта не существует и что он должен создать его. Это не тот случай, если U::operator=только что был вызван.
Здравствуйте, до свидания,

Ответы:


365

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

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

DevX дает хороший пример :

Стандарт C ++ также поддерживает размещение оператора new, который создает объект в заранее выделенном буфере. Это полезно при создании пула памяти, сборщика мусора или просто, когда производительность и безопасность исключений имеют первостепенное значение (нет опасности сбоя выделения, так как память уже выделена, а создание объекта в предварительно выделенном буфере занимает меньше времени) :

char *buf  = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi");    // placement new
string *q = new string("hi");          // ordinary heap allocation

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

Распределение в размещении новых

Вы не должны освобождать каждый объект, который использует буфер памяти. Вместо этого вы должны удалить [] только оригинальный буфер. Затем вам придется вызывать деструкторы ваших классов вручную. Хорошее предложение по этому вопросу см. В разделе часто задаваемых вопросов Страуструпа: « Есть ли удаление места размещения» ?


54
Это не рекомендуется, так как вам нужна эта функция для эффективной реализации объектов-контейнеров (таких как vector). Если вы не создаете свой собственный контейнер, вам не нужно использовать эту функцию.
Мартин Йорк,

26
Также очень важно помнить о #include <memory>, иначе вы можете столкнуться с некоторыми ужасными головными болями на некоторых платформах, которые автоматически не распознают новые места размещения
Ramon Zarazua B.

22
Строго говоря, это неопределенное поведение для вызова delete[]исходного charбуфера. Использование размещения newзакончило время жизни исходных charобъектов, повторно использовав их хранилище. Если вы теперь вызываете delete[] bufдинамический тип объекта (ов), на который указывают, он больше не соответствует их статическому типу, поэтому у вас неопределенное поведение. Более логично использовать operator new/ operator deleteдля выделения необработанной памяти, предназначенной для использования путем размещения new.
CB Bailey

31
Я бы определенно пропустил использование кучи в кардиостимуляторе :-)
Эли Бендерски

15
@RamonZarazua Неправильный заголовок #include <new>.
bit2shift

63

Мы используем его с пользовательскими пулами памяти. Просто набросок:

class Pool {
public:
    Pool() { /* implementation details irrelevant */ };
    virtual ~Pool() { /* ditto */ };

    virtual void *allocate(size_t);
    virtual void deallocate(void *);

    static Pool::misc_pool() { return misc_pool_p; /* global MiscPool for general use */ }
};

class ClusterPool : public Pool { /* ... */ };
class FastPool : public Pool { /* ... */ };
class MapPool : public Pool { /* ... */ };
class MiscPool : public Pool { /* ... */ };

// elsewhere...

void *pnew_new(size_t size)
{
   return Pool::misc_pool()->allocate(size);
}

void *pnew_new(size_t size, Pool *pool_p)
{
   if (!pool_p) {
      return Pool::misc_pool()->allocate(size);
   }
   else {
      return pool_p->allocate(size);
   }
}

void pnew_delete(void *p)
{
   Pool *hp = Pool::find_pool(p);
   // note: if p == 0, then Pool::find_pool(p) will return 0.
   if (hp) {
      hp->deallocate(p);
   }
}

// elsewhere...

class Obj {
public:
   // misc ctors, dtors, etc.

   // just a sampling of new/del operators
   void *operator new(size_t s)             { return pnew_new(s); }
   void *operator new(size_t s, Pool *hp)   { return pnew_new(s, hp); }
   void operator delete(void *dp)           { pnew_delete(dp); }
   void operator delete(void *dp, Pool*)    { pnew_delete(dp); }

   void *operator new[](size_t s)           { return pnew_new(s); }
   void *operator new[](size_t s, Pool* hp) { return pnew_new(s, hp); }
   void operator delete[](void *dp)         { pnew_delete(dp); }
   void operator delete[](void *dp, Pool*)  { pnew_delete(dp); }
};

// elsewhere...

ClusterPool *cp = new ClusterPool(arg1, arg2, ...);

Obj *new_obj = new (cp) Obj(arg_a, arg_b, ...);

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


1
Ага. Мы достаточно умны, но это не по теме для этого вопроса.
Дон Уэйкфилд

2
@jdkoftinoff есть ли у вас ссылка на пример кода? кажется довольно интересным для меня!
Виктор

@DonWakefield Как вы справляетесь с выравниванием в этом пуле? Разве вы не должны передавать выравнивание в качестве аргумента allocate()где-то?
Михаил Васильев

1
@MikhailVasilyev, в реальной реализации вы бы, конечно, с этим справились. Только пример кода.
Дон Уэйкфилд

Что делать, если место размещения является недействительным адресом, скажем, 0x0?
Чарли

51

Это полезно, если вы хотите отделить распределение от инициализации. STL использует размещение новых для создания элементов контейнера.


35

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

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

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


Привет Тед, не могли бы вы рассказать подробнее о решении, которое у вас есть. Я думаю о заранее выделенном решении, но не достиг большого прогресса. Заранее спасибо!
Вьетнам

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

26

Я использовал его для создания объектов, размещенных в стеке, с помощью alloca ().

бесстыдная вилка: я писал об этом здесь .


интересная статья, но я не уверен, что понимаю преимущество использования этого boost::array. Можете ли вы рассказать об этом немного?
GrahamS

boost :: array требует, чтобы размер массива был константой времени компиляции. Это не имеет этого ограничения.
Ферруччо

2
@Ferruccio Это довольно круто, я заметил, что ваш макрос немного небезопасен, а именно размер может быть выражением. Например, если передано x + 1, вы бы расширили его до sizeof (type) * x + 1, что было бы неправильно. Вам нужно закрепить макрос, чтобы сделать его более безопасным.
Бендж

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

14

Главный Компьютерщик: БИНГО! Вы получили это полностью - это именно то, для чего это идеально. Во многих встроенных средах внешние ограничения и / или сценарий общего использования вынуждают программиста отделить выделение объекта от его инициализации. С ++, объединившись, называет это «созданием экземпляра»; но всякий раз, когда действие конструктора должно быть явно вызвано БЕЗ динамического или автоматического выделения, размещение нового - способ сделать это. Это также идеальный способ найти глобальный объект C ++, прикрепленный к адресу аппаратного компонента (ввод-вывод с отображением в памяти) или для любого статического объекта, который по какой-либо причине должен находиться по фиксированному адресу.


12

Я использовал его для создания класса Variant (то есть объекта, который может представлять одно значение, которое может быть одним из множества различных типов).

Если все типы значений, поддерживаемые классом Variant, являются типами POD (например, int, float, double, bool), тогда достаточно тегированного объединения в стиле C, но если вы хотите, чтобы некоторые типы значений были объектами C ++ ( например, std :: string), функция объединения C не подойдет, поскольку типы данных, не относящиеся к POD, не могут быть объявлены как часть объединения.

Поэтому вместо этого я выделяю байтовый массив, который является достаточно большим (например, sizeof (the_largest_data_type_I_support)), и использую размещение new, чтобы инициализировать соответствующий объект C ++ в этой области, когда Variant установлен для хранения значения этого типа. (И размещение удаляется заранее при переключении с другого типа данных, не POD, конечно)


Хм, типы данных, отличные от POD, могут быть объявлены в объединении, при условии, что вы предоставляете ctor объединения - и, эй, - этот ctor , вероятно, будет использовать размещениеnew для инициализации своего подкласса , не являющегося POD. Ссылка: stackoverflow.com/a/33289972/2757035 Изобретать это колесо с использованием произвольно большого массива байтов - это впечатляющая акробатика, но она кажется совершенно ненужной. Итак, что я пропустил? :)
underscore_d

6
Вы пропустили все версии C ++ до C ++ 11, которые во многих случаях все еще необходимо поддерживать. :)
Джереми Фризнер

10

Размещение new также очень полезно при сериализации (скажем, с boost :: serialization). За 10 лет c ++ это только второй случай, для которого мне нужно было разместить новое (третий, если вы включаете интервью :)).


9

Это также полезно, когда вы хотите повторно инициализировать глобальные или статически распределенные структуры.

Старый способ C использовал memset()для установки всех элементов на 0. Вы не можете сделать это в C ++ из-за vtables и пользовательских конструкторов объектов.

Поэтому я иногда использую следующее

 static Mystruct m;

 for(...)  {
     // re-initialize the structure. Note the use of placement new
     // and the extra parenthesis after Mystruct to force initialization.
     new (&m) Mystruct();

     // do-some work that modifies m's content.
 }

1
Разве вам не нужно было бы выполнить соответствующее уничтожение, прежде чем переинициализировать его таким образом?
Глава Geek

[Отредактировано для орфографии] Обычно - вы делаете. Но иногда, когда вы знаете, что класс не выделяет память или другие ресурсы (или вы освобождали их внешне - например, когда вы используете пулы памяти), вы можете использовать эту технику. Это гарантирует, что указатели v-таблицы не будут перезаписаны. - nimrodm 16 часов назад
nimrodm

1
Даже в C использование установки всех битов в 0 гарантировано только для получения представления 0 для целочисленных типов, а не для других типов (нулевой указатель может иметь ненулевое представление).
любопытный парень

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

9

Я думаю, что это не было выделено ни одним ответом, но другой хороший пример и использование для нового размещения - уменьшить фрагментацию памяти (с помощью пулов памяти). Это особенно полезно во встроенных системах и системах с высокой доступностью. В этом последнем случае это особенно важно, потому что для системы, которая должна работать 24/365 дней, очень важно не иметь фрагментации. Эта проблема не имеет ничего общего с утечкой памяти.

Даже когда используется очень хорошая реализация malloc (или аналогичная функция управления памятью), очень трудно долго справляться с фрагментацией. В какой-то момент, если вы не будете умело управлять вызовами резервирования / освобождения памяти, вы можете получить множество небольших пробелов , которые трудно использовать повторно (назначить новым резервированиям). Таким образом, одним из решений, которые используются в этом случае, является использование пула памяти для выделения перед рукой памяти для объектов приложения. После этого каждый раз, когда вам нужна память для какого-либо объекта, вы просто используете новое размещение, чтобы создать новый объект в уже зарезервированной памяти.

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

Я видел это на практике специально для ОСРВ VxWorks, поскольку система распределения памяти по умолчанию сильно страдает от фрагментации. Поэтому выделение памяти с помощью стандартного метода new / malloc в проекте было в основном запрещено. Все резервирования памяти должны идти в выделенный пул памяти.


9

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

Принимать контейнеры нравится unordered_map, vectorили deque. Все они выделяют больше памяти, чем минимально требуется для элементов, которые вы вставили до сих пор, чтобы избежать необходимости выделения кучи для каждой отдельной вставки. Давайте использовать vectorв качестве простейшего примера.

Когда вы делаете:

vector<Foo> vec;

// Allocate memory for a thousand Foos:
vec.reserve(1000);

... это на самом деле не построить тысячу Фоос. Он просто выделяет / резервирует память для них. Если vectorбы здесь не использовалось размещение new, это Foosповсеместно создавало бы по умолчанию, а также вызывало бы их деструкторы даже для элементов, которые вы вообще никогда не вставляли.

Распределение! = Строительство, Освобождение! = Разрушение

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

Должно быть разделение между этими идеями, чтобы избежать ненужного вызова конструкторов и деструкторов без необходимости влево и вправо, и поэтому стандартная библиотека отделяет идею std::allocator(которая не создает и не уничтожает элементы, когда она выделяет / освобождает память *) от контейнеры, которые его используют, которые создают элементы вручную с помощью размещения новых и уничтожают элементы вручную, используя явные вызовы деструкторов.

  • Я ненавижу дизайн, std::allocatorно это другая тема, о которой я не буду рассказывать. :-D

Так или иначе, я имею тенденцию использовать это много, так как я написал много стандартных C ++ контейнеров общего назначения, которые не могли быть построены с точки зрения существующих. В их числе небольшая векторная реализация, которую я создал пару десятилетий назад, чтобы избежать выделения кучи в обычных случаях, и эффективная память (не выделяет один узел за раз). В обоих случаях я не мог по-настоящему реализовать их, используя существующие контейнеры, и поэтому мне пришлось использовать их, placement newчтобы избежать лишнего вызова конструкторов и деструкторов для ненужных вещей слева и справа.

Естественно, если вы когда-либо работаете с пользовательскими распределителями, чтобы распределять объекты по отдельности, например, в виде свободного списка, вам также, как правило, нужно использовать placement newвот так (базовый пример, который не беспокоит исключительная безопасность или RAII):

Foo* foo = new(free_list.allocate()) Foo(...);
...
foo->~Foo();
free_list.free(foo);

8

Это полезно, если вы собираете ядро ​​- куда вы помещаете код ядра, который вы читаете с диска или из таблицы? Вы должны знать, где прыгать.

Или в других, очень редких случаях, например, когда у вас много выделенного помещения и вы хотите разместить несколько конструкций друг за другом. Они могут быть упакованы таким образом без необходимости использования оператора offsetof (). Однако для этого есть и другие приемы.

Я также считаю, что некоторые реализации STL используют новые размещения, такие как std :: vector. Таким образом, они выделяют место для 2 ^ n элементов и не всегда должны перераспределяться.


Сокращение выделения памяти - одна из основных причин ее использования, а также такие хитрости, как загрузка объектов с диска
lefticus

Я не знаю ни одного ядра, написанного на C ++; большинство ядер написаны на прямом C.
Адам Розенфилд

8
Операционная система, с которой я изучил основы ОС, написана на C ++: sweb.sourceforge.net
mstrobl


7

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


7

Я видел, как это использовалось в качестве небольшого снижения производительности для указателя «динамического типа» (в разделе «Под капотом»):

Но вот хитрый прием, который я использовал, чтобы получить быструю производительность для небольших типов: если удерживаемое значение может уместиться в пустоте *, я на самом деле не беспокоюсь о выделении нового объекта, я принудительно помещаю его в сам указатель с помощью размещения нового ,


Что, если удерживаемое значение может уместиться в пустоте * означает? Всегда можно назначить любой тип указателя на void *. Можете ли вы показать нам пример?
anurag86

@ anurag86: На моей 64-битной машине, а void*занимает 8 байтов. Немного глупо указывать восемь байт void*на один байт bool. Но вполне возможно на самом деле наложить boolна void*, очень как union { bool b; void* v }. Вам нужен какой-то способ узнать, что то, что вы называете, на void*самом деле является bool(или a short, или a floatи т. Д.). Статья, на которую я ссылаюсь, описывает, как это сделать. И, чтобы ответить на исходный вопрос, размещение new- это функция, используемая для создания bool(или другого типа), где void*ожидается a (приведения используются для последующего получения / изменения значения).
Макс Либберт

@ anurag86: Это не одно и то же, но вас могут заинтересовать теговые указатели ( en.wikipedia.org/wiki/Tagged_pointer ).
Макс Либберт

6

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


5

Как правило, размещение нового используется, чтобы избавиться от стоимости размещения «нормального нового».

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



4

Единственное место, где я столкнулся с ним, - это контейнеры, которые выделяют непрерывный буфер и затем заполняют его объектами по мере необходимости. Как уже упоминалось, std :: vector может сделать это, и я знаю, что некоторые версии MFC CArray и / или CList делали это (потому что именно там я впервые столкнулся с этим). Метод перераспределения буфера является очень полезной оптимизацией, и размещение new является практически единственным способом создания объектов в этом сценарии. Это также иногда используется для создания объектов в блоках памяти, размещенных вне вашего прямого кода.

Я использовал его в аналогичном качестве, хотя это не часто встречается. Это полезный инструмент для набора инструментов C ++.


4

Механизмы сценариев могут использовать его в собственном интерфейсе для выделения собственных объектов из сценариев. См. Angelscript (www.angelcode.com/angelscript) для примеров.


3

См. Файл fp.h в проекте xll по адресу http://xll.codeplex.com. Он решает проблему «необоснованной болтовни с компилятором» для массивов, которым нравится носить с собой свои размеры.

typedef struct _FP
{
    unsigned short int rows;
    unsigned short int columns;
    double array[1];        /* Actually, array[rows][columns] */
} FP;

2

Вот убийственное использование для конструктора на месте C ++: выравнивание по строке кэша, а также другие степени 2 границ. Вот мой сверхбыстрый алгоритм выравнивания указателя для любой степени 2 границ с 5 или менее инструкциями одного цикла :

/* Quickly aligns the given pointer to a power of two boundary IN BYTES.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param boundary_byte_count The boundary byte count that must be an even
power of 2.
@warning Function does not check if the boundary is a power of 2! */
template <typename T = char>
inline T* AlignUp(void* pointer, uintptr_t boundary_byte_count) {
  uintptr_t value = reinterpret_cast<uintptr_t>(pointer);
  value += (((~value) + 1) & (boundary_byte_count - 1));
  return reinterpret_cast<T*>(value);
}

struct Foo { Foo () {} };
char buffer[sizeof (Foo) + 64];
Foo* foo = new (AlignUp<Foo> (buffer, 64)) Foo ();

Теперь разве это не вызывает улыбку на лице (:-). Я ♥♥♥ C ++ 1x

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.