Які існують можливості для "розміщення нових"?


410

Хтось тут коли-небудь використовував C ++ "розміщення нових"? Якщо так, то для чого? Мені здається, це було б корисно лише на апаратному забезпеченні, нанесеному на пам'ять.


14
Це лише інформація, яку я шукав, для виклику конструкторів об'єктів для збільшення виділених пулів пам'яті. (Сподіваючись, що ці ключові слова полегшать пошук когось у майбутньому).
Sideshow Bob

2
Він використовується в статті Вікіпедії C ++ 11 в конструкторі союзу.
HelloGoodbye

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

@ Andrei-NiculaePetre Я не використовував нове розташування самостійно, але, мабуть, вам слід його використовувати разом із конструктором копій - якщо у вас зараз немає об'єкта цього класу, інакше слід використовувати оператор призначення копії. Якщо тільки клас не тривіальний; то не має значення, яким з них ви користуєтесь. Те ж саме стосується знищення об’єкта. Якщо неправильно впоратися з цим для нетривіальних класів, це може призвести до дивної поведінки, а в деяких ситуаціях навіть може призвести до невизначеної поведінки .
HelloGoodbye

@ Andrei-NiculaePetre Насправді я вважаю приклад у статті Вікіпедії досить поганим, оскільки він просто передбачає, що жодного попереднього об'єкта не існує і що їм потрібно його сконструювати. Це не так, якщо U::operator=щойно зателефонували.
HelloGoodbye

Відповіді:


364

Розміщення new дозволяє побудувати об'єкт у вже виділеній пам'яті.

Ви можете зробити це для оптимізації, коли вам потрібно сконструювати кілька екземплярів об'єкта, і швидше не перерозподіляти пам'ять кожного разу, коли вам потрібен новий екземпляр. Натомість може бути ефективніше виконати єдиний розподіл для шматка пам’яті, який може вмістити декілька об’єктів, навіть якщо ви не хочете використовувати все це відразу.

DevX дає хороший приклад :

Стандартний C ++ також підтримує розміщення нового оператора, який будує об'єкт за попередньо виділеним буфером. Це корисно при створенні пулу пам’яті, сміттєзбірника або просто тоді, коли безпека продуктивності та виключення є першорядною (немає небезпеки виходу з ладу, оскільки пам'ять уже виділена, а побудова об’єкта за попередньо виділеним буфером займає менше часу) :

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

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

Розміщення в місцях розміщення нове

Ви не повинні розміщувати кожен об'єкт, який використовує буфер пам'яті. Натомість слід видалити [] лише вихідний буфер. Тоді вам доведеться викликати руйнівників своїх класів вручну. Щоб отримати гарну пропозицію з цього питання, перегляньте поширені запитання Stroustrup на тему: Чи є "видалення місця розташування" ?


54
Це не застаріло, оскільки вам потрібна ця функція для ефективної реалізації об'єктів контейнера (наприклад, вектора). Якщо ви не будуєте власний контейнер, вам не потрібно використовувати цю функцію.
Мартін Йорк

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

Я використовував це в програмі в режимі реального часу. Зазвичай ми не хочемо виконувати будь-яке динамічне розподіл (або дислокацію) після запуску системи, оскільки немає гарантії, скільки часу це займе.

Що я можу зробити - це виділити великий шматок пам'яті (достатньо великий, щоб вмістити будь-яку кількість всього, що може знадобитися класу). Тоді, коли я під час виконання часу з’ясовую, як конструювати речі, розміщення нових можна використовувати для побудови об’єктів саме там, де я їх хочу. Я знаю, що я використовував цю ситуацію, щоб допомогти створити неоднорідний круговий буфер .

Це, звичайно, не для слабкого серця, але саме тому вони роблять синтаксис для цього своєрідним.


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

1
Ну, власне гетерогенний круговий буферний код був справді складною частиною, щоб правильно вийти. Пальцевий новий виглядає трохи грізно, але порівняно це зовсім не склало проблем.
ТЕД

26

Я використовував його для побудови об'єктів, виділених у стеці через alloca ().

безсоромний штекер: я тут про це блогував .


цікава стаття, але я не впевнений, що я розумію перевагу використання цього над boost::array. Чи можете ви трохи розширити це?
GrahamS

boost :: масив вимагає, щоб розмір масиву був постійною часу компіляції. Цього обмеження не має.
Ferruccio

2
@Ferruccio Це дуже круто, я помітив, що ваш макрос трохи небезпечний, хоча розмір може бути виразом. Якщо, наприклад, передано x + 1, ви розширите його до sizeof (type) * x + 1, що було б невірно. Потрібно закріпити макрос, щоб зробити його безпечнішим.
Бендж

Використання аллока для мене виглядає небезпечно, якщо буде викинуто виняток, оскільки ви повинні викликати деструктори на всіх своїх об'єктах.
CashCow

14

Голова вихованка: BINGO! Ви отримали його цілком - саме для цього ідеально. У багатьох вбудованих середовищах зовнішні обмеження та / або загальний сценарій використання змушує програміста відокремити розподіл об'єкта від його ініціалізації. Скупчившись разом, C ++ називає це "інстанцією"; але щоразу, коли дії конструктора повинні бути явно викликані БЕЗ динамічного чи автоматичного розподілу, спосіб це зробити. Це також ідеальний спосіб знайти глобальний об'єкт 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. Ref: stackoverflow.com/a/33289972/2757035 Повторне винайдення цього колеса за допомогою довільно великого байтового масиву - вражаючий фрагмент акробатики, але здається абсолютно непотрібним. Отже, що я пропустив? :)
підкреслити_

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
Чи не потрібно вам робити відповідне знищення, перш ніж повторно ініціалізувати його таким чином?
Head Geek

[Відредаговано для написання правопису] Зазвичай - ви робите. Але іноді, коли ви знаєте, що клас не виділяє пам'ять або інші ресурси (або ви розміщуєте їх зовні - наприклад, коли ви використовуєте пули пам'яті), ви можете використовувати цю техніку. Це гарантує, що покажчики v-таблиці не будуть перезаписані. - nimrodm 16 годин тому
nimrodm

1
Навіть у C використання параметрів усіх бітів у 0 гарантується лише для створення представлення 0 для інтегральних типів, а не для інших типів (нульовий покажчик може мати нульове подання).
curiousguy

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

9

Насправді потрібна реалізація будь-якої структури даних, яка виділяє більше пам'яті, ніж мінімально потрібно для кількості вставлених елементів (тобто нічого, крім пов'язаної структури, яка виділяє один вузол за один раз).

Приймати контейнери подобається unordered_map, vectorабо deque. Усі вони виділяють більше пам’яті, ніж потрібно мінімально для елементів, які ви вставили до цього часу, щоб уникнути необхідності виділення кучі для кожної окремої вставки. Скористаємося vectorяк найпростіший приклад.

Коли ви робите:

vector<Foo> vec;

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

... що насправді не побудує тисячу Фоос. Він просто виділяє / резервує пам'ять для них. Якщо vectorб тут не було використано нове розміщення, було б побудовано 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

8

Я думаю, що це не було висвітлено жодною відповіддю, але ще одним хорошим прикладом та використанням нового місця розташування є зменшення фрагментації пам'яті (за допомогою пулів пам’яті). Це особливо корисно у вбудованих системах із високою доступністю. В останньому випадку це особливо важливо, оскільки для системи, яка повинна працювати 24/365 днів, дуже важливо не мати фрагментації. Ця проблема не має нічого спільного з витоком пам'яті.

Навіть коли використовується дуже хороша реалізація малокалу (або подібна функція управління пам'яттю), дуже важко боротися з фрагментацією протягом тривалого часу. У якийсь момент, якщо ви не керуєте розумно дзвінками резервування / випуску пам'яті, у вас може виникнути безліч невеликих прогалин , які важко повторно використати (призначити нові бронювання). Отже, одне з рішень, яке використовується в цьому випадку, - використовувати пул пам’яті для виділення перед передачею пам’яті об’єктам програми. Після відключення кожного разу, коли вам потрібна пам'ять для якогось об'єкта, ви просто використовуєте нове розташування, щоб створити новий об'єкт на вже зарезервованій пам'яті.

Таким чином, після запуску вашої програми у вас вже збережена вся необхідна пам'ять. Усі нові резерви пам'яті / випуску пам’яті переходять до виділених пулів (у вас може бути кілька пулів, по одному для кожного різного класу об’єктів). Жодна фрагментація пам'яті не відбувається в цьому випадку, оскільки не буде прогалин, і ваша система може працювати дуже довгі періоди (роки), не страждаючи від фрагментації.

Я це бачив на практиці спеціально для RTX VxWorks, оскільки його система розподілу пам'яті за замовчуванням сильно зазнає фрагментації. Тому розподіл пам'яті за допомогою стандартного методу new / malloc було в основному заборонено в проекті. Усі резерви пам’яті повинні перейти до спеціалізованого пулу пам’яті.



7

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


7

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

Але ось хитрий трюк, який я використовував для отримання швидкої продуктивності для малих типів: якщо значення, яке утримується, може вміститися всередині порожнечі *, я фактично не намагаюся виділити новий об’єкт, я змушую його в самому покажчику, використовуючи розміщення нового .


Що робити, якщо значення, яке утримується, може поміститися всередині порожнечі * ? Завжди можна призначити недійсним будь-який тип вказівника *. Чи можете ви нам показати приклад?
anurag86

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

@ 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-х кордонів. Ось мій ультрашвидкий алгоритм вирівнювання вказівника на будь-яку потужність двох меж з 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 ();

Тепер це не просто поклало посмішку на обличчя (:-). I ♥♥♥ C ++ 1x

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.