Как работает новое размещение C++?

Этот вопрос должен подтвердить, что я правильно понял концепцию, и получить экспертное мнение о стиле использования и возможной оптимизации.

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

 #include <iostream>
 #include <new>

 class A {
 int *_a;
 public:
 A(int v) {std::cout<<"A c'tor clalled\n";_a= new int(v);}
 ~A() {std::cout<<"A d'tor clalled\n"; delete(_a);}
 void testFunction() {std::cout<<"I am a test function &_a = "<<_a<<" a = "<<*_a<<"\n";}
};
int main()
{
    A *obj1 = new A(21);
    std::cout<<"Object allocated at "<<obj1<<std::endl;
    obj1->~A();
    std::cout<<"Object allocated at "<<obj1<<std::endl;
    obj1->testFunction();
    A *obj2 = new(obj1) A(22);
    obj1->testFunction();
    obj2->testFunction();
    delete(obj1);// Is it really needed now? Here it will delete both objects.. so this is not the right place.
    //obj1->testFunction();
    //obj2->testFunction();
    return 0;
}

Когда я запускаю эту программу, я получаю следующее o/p

A c'tor clalled
Object allocated at 0x7f83eb404c30
A d'tor clalled
Object allocated at 0x7f83eb404c30
I am a test function &_a = 0x7f83eb404c40 a = 21
A c'tor clalled
I am a test function &_a = 0x7f83eb404c40 a = 22
I am a test function &_a = 0x7f83eb404c40 a = 22
A d'tor clalled
I am a test function &_a = 0x7f83eb404c40 a = 0
I am a test function &_a = 0x7f83eb404c40 a = 0

У меня следующий вопрос...

  • Является ли это правильным примером для демонстрации нового размещения?
  • элемент a выделяется динамически (без нового размещения). Так почему же он получает один и тот же адрес для obj1 и obj2. Это просто совпадение?
  • Является ли звонок D'tor по линии 15 хорошей практикой?

Пожалуйста, также укажите, что вы видите что-то, что я могу улучшить или просто не пробую. Любая хорошая ссылка или чтение также приветствуются.


person vikrant    schedule 29.01.2016    source источник
comment
Чтобы было ясно, разыменование указателя, который был deleteed, вызывает UB.   -  person AndyG    schedule 29.01.2016
comment
@AndyG Вы имеете в виду @строки 22 и 23?   -  person vikrant    schedule 29.01.2016
comment
На вопросы о поведении - да, примерно так, как вы подозреваете. Строки 22 и 23 через вызов неопределенного поведения.   -  person Niall    schedule 29.01.2016
comment
Соглашаться!! Я добавил их только для того, чтобы посмотреть, в каком они состоянии. Вопрос в пуле 2 все еще беспокоит меня.   -  person vikrant    schedule 29.01.2016
comment
Также в этом виновата строка 9. Вы вызываете деструктор A, который delete соответствует _a, а затем вызываете testFunction, который разыменовывает _a.   -  person AndyG    schedule 29.01.2016
comment
@vikrant Пункт 2 - просто совпадение.   -  person molbdnilo    schedule 29.01.2016
comment
Спасибо, что указали. Я действительно пытался понять новые концепции размещения ... так что не обращайте на это внимания. Впредь буду осторожнее.   -  person vikrant    schedule 29.01.2016
comment
Не используйте std::endl, если вам не нужны дополнительные функции, которые он делает. '\n' начинает новую строку.   -  person Pete Becker    schedule 29.01.2016
comment
В целом; неплохо по-прежнему использовать необработанные указатели и динамическую память, если вы знаете, что делаете. Проблемы или вопросы, возникающие в связи с этой проблемой, заключаются в том, когда, куда, зачем это нужно и кто должен удалять память, чтобы предотвратить утечку памяти. Если вы предполагаете, что пользователь или вызывающий абонент должны удалить память, но они предполагают, что вы это делаете; у тебя утечка памяти. Если вы очищаете всю память вручную, а они предполагают, что вы этого не сделали, и снова вызываете удаление, вы получаете повреждение кучи и необработанные исключения. Разумнее просто использовать умные указатели. ...   -  person Francis Cugler    schedule 29.01.2016
comment
(...продолжение) Есть только один случай, о котором я могу думать, что должно быть приемлемо использовать необработанные указатели с new и delete, и это если вы находитесь внутри функции, которая создает новый объект, заполняет другой объект или структуру его данные, а затем очищаются после завершения работы.   -  person Francis Cugler    schedule 29.01.2016
comment
Пожалуйста, не делайте такие номера строк. Оберните их в комментарии.   -  person T.C.    schedule 29.01.2016


Ответы (3)


Это действительно очень просто: new можно рассматривать как выполнение двух действий:

  1. Выделение памяти.
  2. Размещение-конструкция объекта в выделенной памяти.

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

Таким образом, следующие элементы рассматриваются как эквивалентные:

auto obj1 = new std::string("1");
// ↑ can be thought of as equivalent to ↓ 
auto obj2 = (std::string*)malloc(sizeof(std::string));
new(obj2) std::string("2");

То же самое касается delete:

delete obj1;
// ↑ can be thought of as equivalent to ↓ 
obj2->~string();
free(obj2);

Затем вы можете легко рассуждать обо всем этом, когда видите new и delete такими, какие они есть на самом деле: выделение, за которым следует вызов конструктора, и вызов деструктора, за которым следует освобождение.

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

Таким образом, вы должны отслеживать две вещи по отдельности:

  1. Время жизни памяти.

  2. Время жизни объекта.

В приведенном ниже коде показано, как они независимы друг от друга:

#include <cstdlib>
#include <string>
#include <new>

using std::string;

int main() {
    auto obj = (string*)malloc(sizeof(string));  // memory is allocated
    new(obj) string("1");  // string("1") is constructed
    obj->~string ();       // string("1") is destructed
    new(obj) string("2");  // string("2") is constructed
    obj->~string ();       // string("2") is destructed
    free(obj);             // memory is deallocated
}

Ваша программа имеет UB, если время жизни объекта превышает время жизни памяти. Убедитесь, что память всегда переживает срок службы объекта. Например, это имеет UB:

void ub() {
    alignas(string) char buf[sizeof(string)]; // memory is allocated
    new(buf) string("1");                     // string("1") is constructed
} // memory is deallocated but string("1") outlives the memory!

Но это нормально:

void ub() {
    alignas(string) char buf[sizeof(string)]; // memory is allocated
    new(buf) string("1");                     // string("1") is constructed
    buf->~string();                           // string("1") is destructed
}                                             // memory is deallocated

Обратите внимание, что вам нужно правильно выровнять автоматический буфер, используя alignas. Отсутствие alignas для произвольного типа приводит к UB. Может показаться, что это работает, но это только для того, чтобы ввести вас в заблуждение.

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

struct S {
  char str[10];
}
person Kuba hasn't forgotten Monica    schedule 29.01.2016
comment
Лучше использовать ::operator new(sizeof(std::string)) вместо malloc(sizeof(std::string)) для реальной эквивалентности. То же ::operator delete(pointer) вместо free(pointer). - person Deduplicator; 08.06.2021

Это, вероятно, что-то для CodeReview.SE, позвольте мне немного прокомментировать ваш исходный код, прежде чем я отвечу на ваши вопросы.

A *obj1 = new A(21);
std::cout<<"Object allocated at "<<obj1<<std::endl;
obj1->~A();

Обычно вы никогда не вызываете деструктор для объекта, не созданного с новым размещением. В вашем случае вы разрушаете старый и создаете новый с новым размещением. Несмотря на то, что это работает, вам лучше реализовать некоторые функции reset для сброса вашего объекта, а не разрушать и создавать новый.

17    obj1->testFunction();

Это УБ. Вы уже уничтожили объект, вам не следует вызывать для него какие-либо методы.

18    A *obj2 = new(obj1) A(22);
19    obj1->testFunction();
20    obj2->testFunction();

Это нормально, но обратите внимание, что obj1 и obj2 — это один и тот же объект.

21    delete(obj1);// Is it really needed now? Here it will delete both objects.. so this is not the right place.

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

22    obj1->testFunction();
23    obj2->testFunction();

Это - опять же - UB, не вызывайте методы для деконструированного или удаленного объекта. На ваши вопросы:

элемент _a выделяется динамически (без нового размещения). Итак, почему он получает один и тот же адрес для obj1 и obj2. Это просто совпадение?

Не называйте их obj1 и obj2, потому что эти две переменные указывают на один и тот же объект, но да, это совпадение. После того, как первый объект был уничтожен и освободил эту память, второй выделил тот же объем памяти, который был только что освобожден, и распределитель решил дать вам точно такую ​​​​же память.

Является ли звонок D'tor по линии 15 хорошей практикой?

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

Теперь немного о вашем комментарии после удаления. Давайте посмотрим, что на самом деле делают new и новое размещение.

Новый делает:

  • Выделить память из ОС для нового объекта
  • Вызов конструктора нового объекта, адрес (this) устанавливается в блок памяти, полученный аллокатором.

Удаление делает обратное:

  • Вызов деструктора объекта
  • Освободить кусок памяти

Теперь к месту размещения: новое размещение просто пропускает первый шаг (выделение памяти) и вызывает конструктор этого объекта new с this, установленным на адрес, который вы передали. Таким образом, противоположность положению-новому просто вызывает деструктор, поскольку размещения-удаления не существует.

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

Подробнее о новом размещении темы и о том, когда вызывать деструктор, можно прочитать в часто задаваемых вопросах isocpp< /а>

person tkausl    schedule 29.01.2016
comment
Спасибо за отличное объяснение и указание на проблемы в моем коде. - person vikrant; 18.02.2016

Как работает новое размещение C++?

...

Я пытаюсь понять новое размещение, и следующая программа - это программа, которую я придумал...

Оба ответа превосходны. Но вам же интересно, как это работает, поэтому добавлю пояснение из сборки:


  • A *obj1 = new A(21);:

call operator new(unsigned long)
mov esi, 21
mov rdi, rax
mov rbx, rax
call A::A(int)

  • A *obj2 = new(obj1) A(22);

  mov esi, 22
  mov rdi, rbx
  call A::A(int)

Вот как это работает, достаточно ясно и больше никаких объяснений не требуется, верно?

person 陳 力    schedule 25.05.2018