Что такое placement new в C++?

Ответ

Placement new — это особая форма оператора new, которая не выделяет память, а конструирует объект по заранее предоставленному адресу. Это низкоуровневая операция, используемая для разделения этапов выделения памяти и инициализации объекта.

Основные отличия от обычного new:

  1. Не выделяет память: Вы сами управляете областью памяти (стек, выделенный ранее буфер, shared memory).
  2. Не выбрасывает std::bad_alloc: Поскольку память уже есть, исключение на этапе размещения не генерируется (но конструктор объекта может выбросить своё исключение).
  3. Требует ручного вызова деструктора: Память не освобождается оператором delete, деструктор нужно вызывать явно.

Синтаксис: new (адрес_памяти) Тип(аргументы_конструктора)

Пример использования:

#include <new> // Необходим для placement new
#include <iostream>

class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {
        std::cout << "Constructor called. Value: " << value << 'n';
    }
    ~MyClass() {
        std::cout << "Destructor called.n";
    }
};

int main() {
    // 1. Выделяем память (например, в стеке или из пула)
    alignas(MyClass) char buffer[sizeof(MyClass)];

    // 2. Конструируем объект в выделенной памяти с помощью placement new
    MyClass* obj = new (buffer) MyClass(42);
    std::cout << "Object value: " << obj->value << 'n'; // 42

    // 3. Явно вызываем деструктор (обязательный шаг!)
    obj->~MyClass();

    // Память `buffer` будет освобождена при выходе из области видимости.
    return 0;
}

Типичные сценарии применения:

  • Оптимизация производительности: Создание объектов в заранее выделенном пуле памяти (memory pool) для избежания фрагментации кучи и ускорения аллокаций.
  • Низкоуровневое программирование: Работа с разделяемой памятью (shared memory), аппаратными регистрами или памятью, выделенной средствами C (malloc).
  • Контейнеры: Реализация стандартных контейнеров (например, std::vector), которые выделяют сырую память блоками и затем конструируют в ней объекты.

Важное предупреждение: Необходимо следить за выравниванием (alignment) предоставляемой памяти. Использование alignas или специализированных аллокаторов помогает избежать ошибок выравнивания (misalignment), ведущих к падению производительности или краху программы на некоторых архитектурах.

Ответ 18+ 🔞

Давай разберём эту штуку, которая выглядит как какая-то магия, а на деле — просто способ обмануть систему. Placement new, ёпта.

Представь, что обычный new — это как заказать пиццу. Ты звонишь, говоришь: «Хочу пиццу!», и тебе её привозят — готовую, горячую, в коробке. То есть оператор сам находит свободную память (кухню), готовит там объект (печёт пиццу) и отдаёт тебе указатель (привозит курьером).

А placement new — это когда ты сам приходишь на кухню со своей собственной коробкой (буфером памяти) и говоришь: «Так, повар, блядь, не выделяй мне новую печь, вот прямо в эту мою коробку положи готовую пиццу!». Повар (компилятор) только инициализирует объект по адресу, который ты ему сунул под нос.

Чем это, сука, отличается, если коротко:

  1. Память не ищет. Ты сам её предоставляешь. Из стека, из кучи, из shared memory — вообще похуй, откуда. Главное — адрес.
  2. Исключение std::bad_alloc не летит. Ну а с чего бы? Память-то уже есть. Хотя конструктор объекта сам может выкинуть какую-нибудь дичь, это да.
  3. Деструктор вызывать надо вручную. Вот это, бля, самое важное! Потому что когда ты сам управляешь памятью, система за тебя ничего не сделает. Создал через new (адрес) — обязан сам позвать obj->~MyClass(). Иначе ресурсы потекут, а ты потом будешь охуевать.

Как выглядит эта чёрная магия: new (адрес_памяти) Тип(аргументы_конструктора)

Смотри, как это работает на практике:

#include <new> // Без этого хидера — никуда, тут объявление placement new лежит
#include <iostream>

class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {
        std::cout << "Constructor called. Value: " << value << 'n';
    }
    ~MyClass() {
        std::cout << "Destructor called.n";
    }
};

int main() {
    // 1. Готовим свою «коробку» — сырой буфер в стеке.
    // alignas тут — это чтобы выравнивание было правильное, а то будет тебе UB и пиздец.
    alignas(MyClass) char buffer[sizeof(MyClass)];

    // 2. ВУАЛЯ! Не просим память, а конструируем объект прямо в buffer.
    MyClass* obj = new (buffer) MyClass(42);
    std::cout << "Object value: " << obj->value << 'n'; // 42

    // 3. ОБЯЗАТЕЛЬНЫЙ ШАГ! Сам зовём деструктор, как взрослые.
    obj->~MyClass();

    // Память buffer сама очистится при выходе из main, это же стек.
    return 0;
}

Где эту хуйню применяют?

  • Оптимизация, блядь. Memory pools, кастомные аллокаторы. Чтоб не бегать к ядру за каждой крохой, выделяют один большой кусок (овердохуища памяти), а потом в нём ковыряются placement new, создавая объекты. Фрагментация меньше, скорость выше.
  • Низкоуровневые поехавшие штуки. Работа с shared memory, маппинг файлов в память, или когда память тебе даёт какой-нибудь легаси-код на malloc().
  • Внутри стандартных контейнеров. Тот же std::vector, когда делает reserve(), он выделяет сырую память. А потом, при push_back(), он использует placement new, чтобы сконструировать элемент прямо на своём подготовленном плацдарме. Хитрая жопа, да?

И главное предупреждение, на котором все обжигаются: Выравнивание (alignment). Если твой самопальный буфер не выровнен так, как того хочет тип объекта — получишь невиданный пиздец: от тормозов до мгновенного краша на некоторых архитектурах. Поэтому либо alignas, как в примере, либо используй специальные аллокаторы, которые об этом заботятся. Не игнорируй это, а то будет тебе хиросима, а не программа.