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

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

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

Sideshow Bob 14.09.2011 14:19

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

HelloGoodbye 05.11.2015 13:15

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

Andrei-Niculae Petre 28.11.2018 23:29

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

HelloGoodbye 29.11.2018 02:30

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

HelloGoodbye 29.11.2018 02:44

@HelloGoodbye Я согласен, в вики-статье это эквивалентно использованию назначения или нового размещения. Я все еще не был уверен, в чем разница между ними, и создал простой код, чтобы увидеть различия, и теперь я вижу размещение new, похожее на std :: move (), оно создает объект в заданном месте. pastebin.com/37mdM0cM и, как вы сказали, это полезно, если у вас еще нет этого объекта, и вы создаете его там на месте, иначе вы могли бы просто использовать присваивание.

Andrei-Niculae Petre 30.11.2018 03:33

@ Andrei-NiculaePetre Размещение нового работает как обычное новое, только не выделяет памяти. Т.е. он также создает новый объект и, таким образом, вызывает конструктор создаваемого класса. Это не имеет ничего общего с std::move (хотя вы все равно можете использовать std::move в конструкторе). Оператор = не вызывает никаких конструкторов и не имеет ничего общего с std::move.

HelloGoodbye 30.11.2018 16:28

Недавно я использовал новое размещение для моей реализации мемпула. Таким образом, раньше он просто возвращал новый адрес, теперь он создает объект и возвращает адрес.

arun pal 29.05.2019 10:50

Вот несколько демонстраций / примеров использования превосходно, чтобы проиллюстрировать, как использовать новое размещение и что оно делает! geeksforgeeks.org/placement-new-operator-cpp

Gabriel Staples 13.09.2020 06:37

С опозданием на 12 лет, но размещение new позволяет вам сделать эквивалент копировать не копируемые объекты, даже когда memcpy() не может быть использован, потому что это может привести к неопределенному поведению, поскольку тип объекта нетривиально копируем. См. Мой (сверхдлинный) ответ, который я только что добавил: stackoverflow.com/a/63893849/4561887. Новое размещение - это та волшебная недостающая деталь, в которой я отчаянно нуждался и не знал, как получить раньше без memcpy() (но это было неопределенное поведение для моего нетривиально копируемого объекта)!

Gabriel Staples 15.09.2020 05:34
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
444
10
178 985
24
Перейти к ответу Данный вопрос помечен как решенный

Ответы 24

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

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

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

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

lefticus 21.10.2008 20:40

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

Adam Rosenfield 21.10.2008 20:50

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

mstrobl 21.10.2008 23:13

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

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

Он используется std::vector<>, потому что std::vector<> обычно выделяет больше памяти, чем objects в vector<>.

Ответ принят как подходящий

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

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

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

Standard C++ also supports placement new operator, which constructs an object on a pre-allocated buffer. This is useful when building a memory pool, a garbage collector or simply when performance and exception safety are paramount (there's no danger of allocation failure since the memory has already been allocated, and constructing an object on a pre-allocated buffer takes less time):

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

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

Выделение в размещение новое

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

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

Max Lybbert 22.10.2008 00:20

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

Martin York 22.10.2008 01:19

Что ж, есть несколько возможных применений, например, при десериализации параметрических неизменяемых типов данных.

Marcin 22.10.2008 01:47

Я думаю, точно не устарел.

Brian R. Bondy 22.10.2008 03:25

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

Ramon Zarazua B. 24.09.2009 22:28

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

CB Bailey 21.03.2010 18:10

Что действительно бесполезно, так это размещение массивов с синтаксисом размещения, см. stackoverflow.com/questions/15254/…

mlvljr 21.03.2010 18:12

Полезно различать оператор new () и оператор new (). первый выделяет, а второй конструирует.

Özgür 05.04.2010 22:06

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

Eli Bendersky 09.01.2011 11:42

Это плохой пример предварительного выделения буфера. Показан заголовок фиксированного размера для предварительно выделенного буфера. Буфер все еще находится в куче.

jyoung 13.05.2012 22:18

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

Mark Vincze 28.10.2013 19:14

@CharlesBailey UB? Продолжительность жизни? Вы не понимаете.

curiousguy 14.08.2015 06:14

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

bit2shift 26.04.2016 06:46

Использование строки в качестве примера - худшее из того, что вы могли сделать, потому что существует мысленный конфликт между людьми, чтобы понять объект управления строкой и последовательность байтов, составляющих содержимое строки (которые все еще помещаются в кучу). А отказ от использования префикса std :: as усугубляет ситуацию и сохраняет антипаттерн «использование std» живым и вредным.

Lothar 08.07.2018 15:33

Я просто хочу отметить, что вы также можете предварительно выделить буфер или пул памяти, используя распределение статической памяти, вместо динамического распределения памяти. Пример: в большем объеме, чем в том случае, когда вы будете использовать новое размещение, статически предварительно выделите буфер следующим образом: char buf[sizeof(string)]; вместо этого: char *buf = new char[sizeof(string)];, но используйте это точно так же! Пример: поместите эту строку в этот буфер памяти, используя «размещение нового»: string *p = new (buf) string("hi");. См. Также раздел «Размещение нового» здесь: en.cppreference.com/w/cpp/language/new.

Gabriel Staples 13.09.2020 06:12

Кроме того, чтобы вызвать деструктор вручную, просто выполните: p->~string();. Или, если вы каким-то образом потеряли дескриптор (указатель) p, вы можете вызвать деструктор вручную непосредственно в буфере, до тех пор, пока вы уверены, что действительный объект в настоящее время все еще находится внутри него, и вы знаете его тип!: ((string*)(&buf[0]))->~string();.

Gabriel Staples 13.09.2020 06:18

Я изложил свои соображения выше и многие другие в своем ответе здесь: stackoverflow.com/a/63893849/4561887.

Gabriel Staples 15.09.2020 05:37

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

бесстыдный штекер: Я писал об этом в блоге здесь.

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

GrahamS 10.02.2011 14:52

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

Ferruccio 10.02.2011 15:26

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

Benj 15.03.2012 15:22

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

CashCow 12.04.2013 15:13

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

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, ...);

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

Ага. Мы довольно сообразительны в этом вопросе, но это не по теме этого вопроса.

Don Wakefield 28.01.2011 23:28

@jdkoftinoff есть ли у вас ссылка на реальный образец кода? кажется мне довольно интересным!

Victor 14.01.2017 13:26

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

Mikhail Vasilyev 13.06.2018 20:16

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

Don Wakefield 13.06.2018 20:36

что, если место размещения является недопустимым адресом, скажем 0x0?

Charlie 02.10.2019 07:18

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

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

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

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

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

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

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

Viet 22.03.2010 15:38

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

T.E.D. 23.03.2010 16:40

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

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

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

But here is the tricky trick I used to get fast performance for small types: if the value being held can fit inside of a void*, I don't actually bother allocating a new object, I force it into the pointer itself using placement new.

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

anurag86 20.10.2015 17:06

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

Max Lybbert 20.10.2015 19:12

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

Max Lybbert 20.10.2015 19:13

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

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

См. Файл 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;

Это может быть удобно при использовании разделяемой памяти, среди прочего ... Например: http://www.boost.org/doc/libs/1_51_0/doc/html/interprocess/synchronization_mechanisms.html#interprocess.synchronization_mechanisms.conditions.conditions_anonymous_example

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

Старый способ 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.
 }

Разве вам не нужно было бы произвести соответствующее уничтожение перед повторной инициализацией таким образом?

Head Geek 05.04.2013 07:17

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

nimrodm 06.04.2013 22:54

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

curiousguy 16.08.2015 17:20

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

nimrodm 16.08.2015 17:27

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

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

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

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

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

underscore_d 20.11.2015 03:50

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

Jeremy Friesner 23.11.2015 05:33

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

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

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

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

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

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

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

vector<Foo> vec;

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

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

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

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

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

  • I hate the design of std::allocator but that's a different subject I'll avoid ranting about. :-D

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

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

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

Вот убийственное использование конструктора C++ на месте: выравнивание по строке кеша, а также другие полномочия двух границ. Вот мой сверхбыстрый алгоритм выравнивания указателя до любой степени двух границ с 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

У меня тоже есть идея. В C++ есть принцип нулевых накладных расходов. Но исключения не следуют этому принципу, поэтому иногда они отключаются переключателем компилятора.

Давайте посмотрим на этот пример:

#include <new>
#include <cstdio>
#include <cstdlib>

int main() {
    struct A {
        A() {
            printf("A()\n");
        }
        ~A() {
            printf("~A()\n");
        }
        char data[1000000000000000000] = {}; // some very big number
    };

    try {
        A *result = new A();
        printf("new passed: %p\n", result);
        delete result;
    } catch (std::bad_alloc) {
        printf("new failed\n");
    }
}

Мы размещаем здесь большую структуру, проверяем, успешно ли выделено, и удаляем ее.

Но если у нас отключены исключения, мы не сможем использовать блок try и не сможем обработать ошибку new [].

Итак, как мы можем это сделать? Вот как:

#include <new>
#include <cstdio>
#include <cstdlib>

int main() {
    struct A {
        A() {
            printf("A()\n");
        }
        ~A() {
            printf("~A()\n");
        }
        char data[1000000000000000000] = {}; // some very big number
    };

    void *buf = malloc(sizeof(A));
    if (buf != nullptr) {
        A *result = new(buf) A();
        printf("new passed: %p\n", result);
        result->~A();
        free(result);
    } else {
        printf("new failed\n");
    }
}
  • Используйте простой malloc
  • Проверьте, не удалось ли это способом C
  • В случае успеха мы используем новое размещение.
  • Вызов деструктора вручную (мы не можем просто вызвать удаление)
  • Звоните бесплатно, потому что мы вызвали malloc

UPD@Бесполезный написал комментарий, который открыл мне, что существует новый (без стрелы), который следует использовать в этом случае, но не метод, который я написал ранее. Пожалуйста, не используйте код, который я написал ранее. Извиняюсь.

Конечно, вы могли бы просто использовать new(nothrow)?

Useless 04.01.2021 20:06

На самом деле @ бесполезно, ты здесь. Я даже не знал про nothrow. Как я вижу, мой ответ можно выбросить на помойку. Как вы думаете, я должен убрать ответ?

CPPCPPCPPCPPCPPCPPCPPCPPCPPCPP 04.01.2021 20:10

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

Useless 04.01.2021 20:50

@Useless Внес правку. Спасибо.

CPPCPPCPPCPPCPPCPPCPPCPPCPPCPP 04.01.2021 21:58

У меня есть еще одна идея (она актуальна для C++ 11).

Давайте посмотрим на следующий пример:

#include <cstddef>
#include <cstdio>

int main() {
    struct alignas(0x1000) A {
        char data[0x1000];
    };

    printf("max_align_t: %zu\n", alignof(max_align_t));

    A a;
    printf("a: %p\n", &a);

    A *ptr = new A;
    printf("ptr: %p\n", ptr);
    delete ptr;
}

В стандарте C++ 11 GCC дает следующий выход:

max_align_t: 16
a: 0x7ffd45e6f000
ptr: 0x1fe3ec0

ptr не выровнен должным образом.

В стандарте C++ 17 и выше GCC дает следующий выход:

max_align_t: 16
a: 0x7ffc924f6000
ptr: 0x9f6000

ptr выровнен правильно.

Насколько мне известно, стандарт C++ не поддерживал новое выравнивание до появления C++ 17, и если ваша структура имеет выравнивание больше, чем max_align_t, у вас могут возникнуть проблемы. Чтобы обойти эту проблему в C++ 11, вы можете использовать aligned_alloc.

#include <cstddef>
#include <cstdlib>
#include <cstdio>
#include <new>

int main() {
    struct alignas(0x1000) A {
        char data[0x1000];
    };

    printf("max_align_t: %zu\n", alignof(max_align_t));

    A a;
    printf("a: %p\n", &a);

    void *buf = aligned_alloc(alignof(A), sizeof(A));
    if (buf == nullptr) {
        printf("aligned_alloc() failed\n");
        exit(1);
    }
    A *ptr = new(buf) A();
    printf("ptr: %p\n", ptr);
    ptr->~A();
    free(ptr);
}

ptr в данном случае является выровнен.

max_align_t: 16
a: 0x7ffe56b57000
ptr: 0x2416000

Другие вопросы по теме