Placement New and Memory Pools

Master C++ placement new — learn how to construct objects in pre-allocated memory, build memory pools, manage object lifetimes manually, and avoid common pitfalls.

Placement New and Memory Pools

Placement new is a form of the new operator in C++ that constructs an object at a specific memory address you provide, rather than allocating new memory from the heap. The syntax is new (ptr) Type(args...) — it calls the constructor of Type at the address ptr without allocating any memory. Paired with an explicit destructor call (ptr->~Type()), placement new gives you complete manual control over where objects live and when they are constructed and destroyed — the foundation of all memory pool implementations.

Introduction

In C++, new does two things: it allocates memory from the heap, then it constructs an object in that memory. These two operations are inseparable in ordinary usage. But there are important scenarios where you want them separated:

You have a pre-allocated memory pool and want to construct objects within it, without any additional heap allocation. You have a large buffer and want to initialize elements lazily — only constructing objects when they are actually needed. You are implementing a container like std::vector, which must reserve capacity (allocate raw memory) separately from initializing elements (constructing objects). You want to reuse the same memory for a succession of different objects without allocating and freeing between each use.

All of these scenarios require placement new — the ability to invoke a constructor on already-allocated memory. Placement new is the low-level primitive that makes memory pools, object recyclers, variant types, and the standard library’s own containers possible.

This article teaches placement new from its basic mechanics through advanced memory pool implementations. You will understand the correct construction-and-destruction lifecycle for manually managed objects, see how std::vector uses placement new internally, build a complete type-safe object pool, and understand the alignment requirements that make placement new work correctly across all types.

Regular new vs. Placement new

Understanding placement new starts with understanding what regular new actually does:

C++
#include <iostream>
#include <new>       // Required for placement new
using namespace std;

class Widget {
public:
    int id;
    string name;

    Widget(int i, const string& n) : id(i), name(n) {
        cout << "Widget(" << id << ", " << name << ") constructed at "
             << (void*)this << endl;
    }
    ~Widget() {
        cout << "Widget(" << id << ", " << name << ") destroyed at "
             << (void*)this << endl;
    }
};

int main() {
    cout << "=== Regular new ===" << endl;
    // Step 1: allocates sizeof(Widget) bytes from heap
    // Step 2: calls Widget(1, "Alpha") constructor
    Widget* w1 = new Widget(1, "Alpha");
    // Step 1: calls ~Widget()
    // Step 2: returns memory to heap
    delete w1;

    cout << "\n=== Placement new ===" << endl;
    // Step 1: we allocate memory manually (properly aligned)
    alignas(Widget) char buffer[sizeof(Widget)];

    cout << "Buffer at: " << (void*)buffer << endl;

    // Step 2: construct Widget in our buffer — NO heap allocation
    Widget* w2 = new (buffer) Widget(2, "Beta");
    // w2 points to the same address as buffer

    cout << "w2 == buffer? " << (w2 == (Widget*)buffer ? "YES" : "NO") << endl;

    // Step 3: MUST call destructor explicitly — delete w2 would be WRONG
    // (delete would try to free memory we didn't allocate with new)
    w2->~Widget();  // Explicit destructor call

    // buffer is now available for reuse — no deallocation needed
    cout << "\n=== Reusing the same buffer ===" << endl;
    Widget* w3 = new (buffer) Widget(3, "Gamma");  // Reuse buffer
    w3->~Widget();

    cout << "\n=== Stack-allocated array of objects ===" << endl;
    // Allocate raw memory for 3 Widgets — none are constructed yet
    alignas(Widget) char arr[sizeof(Widget) * 3];

    // Construct them one by one in the array
    for (int i = 0; i < 3; i++) {
        new (arr + i * sizeof(Widget)) Widget(i + 10, "Item" + to_string(i));
    }

    // Access them as an array
    Widget* widgets = reinterpret_cast<Widget*>(arr);
    for (int i = 0; i < 3; i++) {
        cout << "  widgets[" << i << "]: id=" << widgets[i].id
             << " name=" << widgets[i].name << endl;
    }

    // Destroy in reverse order (good practice for objects with dependencies)
    for (int i = 2; i >= 0; i--) {
        widgets[i].~Widget();
    }

    return 0;
}

Output:

Plaintext
=== Regular new ===
Widget(1, Alpha) constructed at 0x55a3b2c01eb0
Widget(1, Alpha) destroyed at 0x55a3b2c01eb0

=== Placement new ===
Buffer at: 0x7ffd2c4b3a80
Widget(2, Beta) constructed at 0x7ffd2c4b3a80
w2 == buffer? YES
Widget(2, Beta) destroyed at 0x7ffd2c4b3a80

=== Reusing the same buffer ===
Widget(3, Gamma) constructed at 0x7ffd2c4b3a80
Widget(3, Gamma) destroyed at 0x7ffd2c4b3a80

=== Stack-allocated array of objects ===
Widget(10, Item0) constructed at 0x7ffd2c4b3aa0
Widget(11, Item1) constructed at 0x7ffd2c4b3ac8
Widget(12, Item2) constructed at 0x7ffd2c4b3af0
  widgets[0]: id=10 name=Item0
  widgets[1]: id=11 name=Item1
  widgets[2]: id=12 name=Item2
Widget(12, Item2) destroyed at 0x7ffd2c4b3af0
Widget(11, Item1) destroyed at 0x7ffd2c4b3ac8
Widget(10, Item0) destroyed at 0x7ffd2c4b3aa0

Step-by-step explanation:

  1. new (buffer) Widget(2, "Beta") is placement new syntax. The (buffer) part is the memory address — no heap allocation happens. The Widget(2, "Beta") part calls the constructor at that address. The return value is the same address cast to Widget*.
  2. w2->~Widget() is an explicit destructor call — the only time in C++ you should call a destructor directly. This is mandatory when using placement new: the destructor cleans up the object’s resources (the string name member here) without freeing the memory. If you used delete w2, the program would attempt to free memory it doesn’t own — undefined behavior.
  3. After w2->~Widget(), the buffer memory is in an indeterminate state — the object no longer exists there, but the raw bytes remain. You can construct a new object there immediately with placement new again.
  4. alignas(Widget) char buffer[sizeof(Widget)] ensures the buffer is aligned to Widget‘s alignment requirement. Without alignas, the buffer’s alignment is only char (1 byte), which may be insufficient for Widget on some architectures, causing undefined behavior.
  5. The array example constructs objects at computed offsets: arr + i * sizeof(Widget). Each Widget lands at the correct position. After use, they are destroyed in reverse order — a good practice when objects might have dependencies on each other.

Alignment: The Critical Requirement

Alignment is the most important correctness requirement for placement new. Every type has an alignment requirement — the address of any object of that type must be a multiple of its alignment.

C++
#include <iostream>
#include <new>
#include <memory>
using namespace std;

// Types with different alignment requirements
struct Byte1  { char x; };           // alignment: 1
struct Int4   { int x; };            // alignment: 4
struct Double8{ double x; };         // alignment: 8
struct SIMD16 {                      // alignment: 16 (for SIMD operations)
    alignas(16) float data[4];
};

void demonstrateAlignment() {
    cout << "=== Alignment requirements ===" << endl;
    cout << "Byte1:   size=" << sizeof(Byte1)   << " align=" << alignof(Byte1)   << endl;
    cout << "Int4:    size=" << sizeof(Int4)     << " align=" << alignof(Int4)    << endl;
    cout << "Double8: size=" << sizeof(Double8)  << " align=" << alignof(Double8) << endl;
    cout << "SIMD16:  size=" << sizeof(SIMD16)   << " align=" << alignof(SIMD16)  << endl;

    // WRONG: unaligned buffer for an int
    char rawBuf[sizeof(Int4) + 4];       // Extra space for alignment adjustment
    char* unaligned = rawBuf + 1;        // Deliberately misaligned
    // new (unaligned) Int4{42};         // UB on strict-alignment architectures!

    // CORRECT: std::align to find the next aligned address
    void*  alignedPtr = rawBuf;
    size_t space      = sizeof(rawBuf);
    bool   ok         = (std::align(alignof(Int4), sizeof(Int4), alignedPtr, space) != nullptr);
    cout << "\nAligned Int4 ptr: " << alignedPtr
         << " (aligned=" << ok << ")" << endl;
    Int4* i = new (alignedPtr) Int4{42};
    cout << "Value: " << i->x << endl;
    i->~Int4();

    // CORRECT: alignas ensures correct alignment from the start
    alignas(SIMD16) char simdBuf[sizeof(SIMD16)];
    SIMD16* sv = new (simdBuf) SIMD16{{1.0f, 2.0f, 3.0f, 4.0f}};
    cout << "\nSIMD values: "
         << sv->data[0] << " "
         << sv->data[1] << " "
         << sv->data[2] << " "
         << sv->data[3] << endl;
    sv->~SIMD16();
}

// Safe buffer type that handles alignment automatically
template<typename T>
struct AlignedStorage {
    alignas(T) char buffer[sizeof(T)];

    // Construct T in place
    template<typename... Args>
    T* construct(Args&&... args) {
        return new (buffer) T(forward<Args>(args)...);
    }

    // Destroy T in place
    void destroy(T* ptr) {
        ptr->~T();
    }

    T* get() { return reinterpret_cast<T*>(buffer); }
    const T* get() const { return reinterpret_cast<const T*>(buffer); }
};

int main() {
    demonstrateAlignment();

    cout << "\n=== AlignedStorage ===" << endl;
    AlignedStorage<Widget> storage;  // Properly aligned, uninitialized

    cout << "Before construct: (uninitialized)" << endl;
    Widget* w = storage.construct(99, "AlignedStorage demo");
    cout << "After construct: id=" << w->id << " name=" << w->name << endl;
    storage.destroy(w);
    cout << "After destroy" << endl;

    return 0;
}

Output:

Plaintext
=== Alignment requirements ===
Byte1:   size=1 align=1
Int4:    size=4 align=4
Double8: size=8 align=8
SIMD16:  size=16 align=16

Aligned Int4 ptr: 0x7ffd... (aligned=1)
Value: 42

SIMD values: 1 2 3 4

=== AlignedStorage ===
Before construct: (uninitialized)
Widget(99, AlignedStorage demo) constructed at 0x7ffd...
After construct: id=99 name=AlignedStorage demo
Widget(99, AlignedStorage demo) destroyed at 0x7ffd...
After destroy

Step-by-step explanation:

  1. alignof(T) returns the alignment requirement of type T as a size_t. A double must be at 8-byte boundaries, a 16-byte SIMD type must be at 16-byte boundaries. Constructing an object at a misaligned address is undefined behavior — on x86 it often works (with a performance penalty), but on ARM and RISC-V it crashes.
  2. alignas(T) char buffer[sizeof(T)] is the correct idiom for a buffer that can hold a T. alignas ensures the buffer starts at an address with at least alignof(T) alignment. This is so common that the standard provides std::aligned_storage<sizeof(T), alignof(T)> (deprecated in C++23) and recommends the alignas pattern directly.
  3. std::align(alignment, size, ptr, space) finds the next aligned address within a buffer. If the buffer has room for the aligned object, it adjusts ptr to the aligned address, reduces space by the alignment padding, and returns ptr. Returns nullptr if there is not enough space.
  4. AlignedStorage<T> is a utility that packages the buffer and construction/destruction operations together. The standard library’s std::optional and std::variant use similar internal storage.

How std::vector Uses Placement New

std::vector is the best example of placement new in production use. A vector maintains a buffer that may have more capacity than the number of live elements. When you push_back, the vector constructs a new element in the unused capacity with placement new. When you pop_back, it destroys the element with an explicit destructor call. reserve allocates raw memory without constructing any objects.

C++
#include <iostream>
#include <new>
#include <algorithm>
using namespace std;

// Simplified vector implementation showing placement new usage
template<typename T>
class SimpleVector {
public:
    SimpleVector() : data_(nullptr), size_(0), capacity_(0) {}

    ~SimpleVector() {
        // Destroy all live elements
        for (size_t i = 0; i < size_; i++) {
            data_[i].~T();  // Explicit destructor — placement new objects
        }
        // Free raw memory
        ::operator delete(data_);
    }

    // Reserve capacity: allocates raw memory, constructs NOTHING
    void reserve(size_t newCap) {
        if (newCap <= capacity_) return;

        // Allocate new raw memory (properly aligned for T)
        T* newData = static_cast<T*>(
            ::operator new(newCap * sizeof(T)));

        // Move existing elements into new memory using placement new
        for (size_t i = 0; i < size_; i++) {
            new (newData + i) T(move(data_[i]));  // Move construct
            data_[i].~T();                          // Destroy old
        }

        ::operator delete(data_);
        data_     = newData;
        capacity_ = newCap;
    }

    // Push: construct new element at end using placement new
    void push_back(const T& value) {
        if (size_ == capacity_) grow();
        new (data_ + size_) T(value);  // Placement new: copy construct
        ++size_;
    }

    void push_back(T&& value) {
        if (size_ == capacity_) grow();
        new (data_ + size_) T(move(value));  // Placement new: move construct
        ++size_;
    }

    // Emplace: construct in-place with forwarded arguments
    template<typename... Args>
    T& emplace_back(Args&&... args) {
        if (size_ == capacity_) grow();
        T* p = new (data_ + size_) T(forward<Args>(args)...);
        ++size_;
        return *p;
    }

    // Pop: destroy last element, DON'T deallocate
    void pop_back() {
        if (size_ == 0) return;
        --size_;
        data_[size_].~T();  // Explicit destructor call
    }

    T&       operator[](size_t i)       { return data_[i]; }
    const T& operator[](size_t i) const { return data_[i]; }
    size_t   size()     const { return size_; }
    size_t   capacity() const { return capacity_; }
    bool     empty()    const { return size_ == 0; }

private:
    void grow() {
        reserve(capacity_ == 0 ? 1 : capacity_ * 2);
    }

    T*     data_;
    size_t size_;
    size_t capacity_;
};

struct Resource {
    int id;
    Resource(int i) : id(i) {
        cout << "  Construct Resource(" << id << ")" << endl;
    }
    Resource(const Resource& o) : id(o.id) {
        cout << "  Copy Resource(" << id << ")" << endl;
    }
    Resource(Resource&& o) : id(o.id) {
        cout << "  Move Resource(" << id << ")" << endl;
        o.id = -1;
    }
    ~Resource() {
        if (id != -1) cout << "  Destroy Resource(" << id << ")" << endl;
    }
};

int main() {
    cout << "=== SimpleVector construction trace ===" << endl;
    SimpleVector<Resource> v;
    v.reserve(4);   // Allocates memory for 4, constructs nothing

    cout << "\nPush 1:" << endl;
    v.push_back(Resource(1));  // Construct temporary, copy/move into vector

    cout << "\nEmplace 2:" << endl;
    v.emplace_back(2);  // Construct directly in-place — no temporary

    cout << "\nEmplace 3:" << endl;
    v.emplace_back(3);

    cout << "\nPop back:" << endl;
    v.pop_back();  // Destroys Resource(3), capacity remains

    cout << "\nEmplace 4 (reuses slot):" << endl;
    v.emplace_back(4);  // Constructs Resource(4) in the slot vacated by 3

    cout << "\nVector contents: size=" << v.size()
         << " capacity=" << v.capacity() << endl;
    for (size_t i = 0; i < v.size(); i++) {
        cout << "  v[" << i << "] = " << v[i].id << endl;
    }

    cout << "\nVector going out of scope:" << endl;
    // Destructor will call ~Resource() for each live element
    return 0;
}

Output:

Plaintext
=== SimpleVector construction trace ===

Push 1:
  Construct Resource(1)
  Move Resource(1)
  Destroy Resource(-1)

Emplace 2:
  Construct Resource(2)

Emplace 3:
  Construct Resource(3)

Pop back:
  Destroy Resource(3)

Emplace 4 (reuses slot):
  Construct Resource(4)

Vector contents: size=3 capacity=4
  v[0] = 1
  v[1] = 2
  v[2] = 4

Vector going out of scope:
  Destroy Resource(1)
  Destroy Resource(2)
  Destroy Resource(4)

Step-by-step explanation:

  1. reserve(4) calls ::operator new(4 * sizeof(Resource)) — raw memory allocation. No constructors are called. The memory holds 4 Resource-sized blocks of uninitialized bytes.
  2. emplace_back(2) calls new (data_ + 1) Resource(2) — constructs Resource directly at slot 1 with the arguments forwarded. No temporary is created. This is why emplace_back is more efficient than push_back for complex types.
  3. pop_back() calls data_[2].~Resource() — the destructor runs and Resource(3) is cleaned up. The memory at slot 2 is not freed — it remains allocated as part of the vector’s capacity. The next emplace_back will reuse it.
  4. The destructor iterates size_ (not capacity_) and calls the destructor for each live element. Only elements that were actually constructed (via placement new) get their destructors called. The raw memory between size_ and capacity_ is freed without destructing — correct, because no objects live there.
  5. This separation of memory allocation from object construction is the foundation of std::vector‘s efficiency: elements beyond the current size don’t need to be default-constructed when you reserve, and pop_back is O(1) regardless of type complexity.

Building a Complete Memory Pool

Now let’s combine placement new with the pool allocator concept to build a complete, type-safe, fixed-size object pool suitable for production use.

C++
#include <iostream>
#include <array>
#include <bitset>
#include <stdexcept>
#include <cassert>
#include <new>
using namespace std;

// Type-safe fixed-size object pool using placement new
template<typename T, size_t Capacity>
class ObjectPool {
public:
    ObjectPool() : usedCount_(0) {
        // Initialize free list: slot i points to slot i+1
        for (size_t i = 0; i < Capacity - 1; i++) {
            freeList_[i] = i + 1;
        }
        freeList_[Capacity - 1] = INVALID;
        freeHead_ = 0;
    }

    ~ObjectPool() {
        // Warn if objects are still live (memory leak in caller)
        if (usedCount_ > 0) {
            cerr << "ObjectPool<" << typeid(T).name()
                 << ">: " << usedCount_ << " objects leaked!" << endl;
            // Optionally: destroy all live objects here for cleanup
            // (would need to track which slots are used)
        }
    }

    // Construct a T in the pool and return a pointer to it
    template<typename... Args>
    T* construct(Args&&... args) {
        if (freeHead_ == INVALID) {
            throw bad_alloc();  // Pool exhausted
        }

        size_t slot    = freeHead_;
        freeHead_      = freeList_[slot];
        inUse_[slot]   = true;
        ++usedCount_;

        // Placement new: construct T in our pre-allocated slot
        return new (slotPtr(slot)) T(forward<Args>(args)...);
    }

    // Destroy a T and return its slot to the pool
    void destroy(T* ptr) {
        size_t slot = slotIndex(ptr);
        assert(slot < Capacity && "Pointer not from this pool");
        assert(inUse_[slot] && "Double-free detected");

        ptr->~T();           // Explicit destructor call

        inUse_[slot]  = false;
        freeList_[slot] = freeHead_;
        freeHead_      = slot;
        --usedCount_;
    }

    // Check if a pointer was allocated from this pool
    bool owns(const T* ptr) const {
        if (ptr < slotPtr(0) || ptr >= slotPtr(Capacity)) return false;
        size_t slot = slotIndex(ptr);
        return slot < Capacity && inUse_[slot];
    }

    size_t used()      const { return usedCount_; }
    size_t available() const { return Capacity - usedCount_; }
    size_t capacity()  const { return Capacity; }

private:
    // Each slot is properly aligned storage for one T
    struct alignas(T) Slot {
        char data[sizeof(T)];
    };

    T* slotPtr(size_t i) const {
        return reinterpret_cast<T*>(const_cast<Slot*>(&slots_[i]));
    }

    size_t slotIndex(const T* ptr) const {
        const Slot* slotArray = slots_.data();
        size_t offset = reinterpret_cast<const Slot*>(ptr) - slotArray;
        return offset;
    }

    static constexpr size_t INVALID = SIZE_MAX;

    array<Slot, Capacity>   slots_;       // Pre-allocated storage
    array<size_t, Capacity> freeList_;    // Free list: next free slot index
    bitset<Capacity>         inUse_;      // Which slots have live objects
    size_t                   freeHead_;   // Index of first free slot
    size_t                   usedCount_;
};

// Example: game entity pool
struct Enemy {
    int   id;
    float x, y;
    int   health;
    char  type[16];

    Enemy(int id, float x, float y, int hp, const char* t)
        : id(id), x(x), y(y), health(hp) {
        strncpy(type, t, 15);
        type[15] = '\0';
        cout << "  Enemy " << id << " (" << type << ") spawned at ("
             << x << "," << y << ")" << endl;
    }

    ~Enemy() {
        cout << "  Enemy " << id << " (" << type << ") despawned" << endl;
    }

    void update(float dt) {
        x += 1.0f * dt;
        y += 0.5f * dt;
        health -= 1;
    }
};

int main() {
    ObjectPool<Enemy, 16> enemyPool;

    cout << "Pool: " << enemyPool.capacity() << " capacity, "
         << enemyPool.available() << " available" << endl;

    cout << "\n--- Spawning enemies ---" << endl;
    Enemy* e1 = enemyPool.construct(1, 10.0f, 20.0f, 100, "Goblin");
    Enemy* e2 = enemyPool.construct(2, 50.0f, 30.0f, 200, "Troll");
    Enemy* e3 = enemyPool.construct(3, 80.0f, 10.0f, 50,  "Archer");

    cout << "\nPool: " << enemyPool.used() << " used, "
         << enemyPool.available() << " available" << endl;

    cout << "\n--- Game loop (3 frames) ---" << endl;
    for (int frame = 0; frame < 3; frame++) {
        e1->update(0.016f); e2->update(0.016f); e3->update(0.016f);
    }

    cout << "\nAfter 3 frames:"
         << "\n  e1: (" << e1->x << "," << e1->y << ") hp=" << e1->health
         << "\n  e2: (" << e2->x << "," << e2->y << ") hp=" << e2->health
         << "\n  e3: (" << e3->x << "," << e3->y << ") hp=" << e3->health << endl;

    cout << "\n--- Despawning e1 and e3 ---" << endl;
    enemyPool.destroy(e1);  // e1 is destroyed; its slot is returned to pool
    enemyPool.destroy(e3);
    e1 = nullptr;
    e3 = nullptr;

    cout << "\nPool: " << enemyPool.used() << " used, "
         << enemyPool.available() << " available" << endl;

    cout << "\n--- Spawning new enemies (reusing slots) ---" << endl;
    Enemy* e4 = enemyPool.construct(4, 15.0f, 25.0f, 150, "Orc");
    Enemy* e5 = enemyPool.construct(5, 45.0f, 35.0f, 80,  "Skeleton");

    cout << "\n  e4 address: " << (void*)e4 << endl;
    cout << "  e1 was at:  " << "(already freed)" << endl;
    cout << "  Same slot reused: pool efficiently recycles memory" << endl;

    cout << "\n--- Cleanup ---" << endl;
    enemyPool.destroy(e2);
    enemyPool.destroy(e4);
    enemyPool.destroy(e5);

    cout << "\nPool: " << enemyPool.used() << " used, "
         << enemyPool.available() << " available" << endl;

    return 0;
}

Output:

Plaintext
Pool: 16 capacity, 16 available

--- Spawning enemies ---
  Enemy 1 (Goblin) spawned at (10,20)
  Enemy 2 (Troll) spawned at (50,30)
  Enemy 3 (Archer) spawned at (80,10)

Pool: 3 used, 13 available

--- Game loop (3 frames) ---

After 3 frames:
  e1: (10.048,20.024) hp=97
  e2: (50.048,30.024) hp=197
  e3: (80.048,10.024) hp=47

--- Despawning e1 and e3 ---
  Enemy 1 (Goblin) despawned
  Enemy 3 (Archer) despawned

Pool: 1 used, 15 available

--- Spawning new enemies (reusing slots) ---
  Enemy 4 (Orc) spawned at (15,25)
  Enemy 5 (Skeleton) spawned at (45,35)

  e4 address: 0x...
  e1 was at:  (already freed)
  Same slot reused: pool efficiently recycles memory

--- Cleanup ---
  Enemy 2 (Troll) despawned
  Enemy 4 (Orc) despawned
  Enemy 5 (Skeleton) despawned

Pool: 0 used, 16 available

Step-by-step explanation:

  1. construct(1, 10.0f, 20.0f, 100, "Goblin") pops a slot from the free list (O(1)), marks it in-use, and calls new (slotPtr(slot)) Enemy(...). The Enemy constructor runs in place — no heap allocation.
  2. destroy(e1) calls e1->~Enemy() explicitly, then pushes the slot back onto the free list (O(1)). The memory is not freed — it stays in the pool’s pre-allocated buffer, ready for the next construct call.
  3. The bitset<Capacity> inUse_ tracks which slots hold live objects. The assert(inUse_[slot]) in destroy catches double-free bugs at runtime — a common error in manual memory management.
  4. The owns(ptr) method verifies that a pointer came from this pool by checking whether it falls within the pool’s storage range and whether the corresponding slot is in use.
  5. When enemies are despawned and new ones spawned, the same physical memory slots are reused. The game does not interact with malloc/free during normal gameplay — every enemy allocation and deallocation is O(1) and operates entirely within the pool’s pre-allocated memory.

Placement new for Optional-Like Storage

std::optional is another production example of placement new: it holds either a value or nothing, using a fixed-size buffer that can contain the value type. The value is constructed and destroyed with placement new as the optional is assigned and reset.

C++
#include <iostream>
#include <new>
#include <utility>
using namespace std;

// Simplified optional<T> implementation
template<typename T>
class SimpleOptional {
public:
    SimpleOptional() : hasValue_(false) {}

    // Construct with a value
    explicit SimpleOptional(const T& val) : hasValue_(false) {
        new (storage_.data) T(val);
        hasValue_ = true;
    }

    explicit SimpleOptional(T&& val) : hasValue_(false) {
        new (storage_.data) T(move(val));
        hasValue_ = true;
    }

    // Copy constructor
    SimpleOptional(const SimpleOptional& other) : hasValue_(false) {
        if (other.hasValue_) {
            new (storage_.data) T(*other);
            hasValue_ = true;
        }
    }

    // Move constructor
    SimpleOptional(SimpleOptional&& other) : hasValue_(false) {
        if (other.hasValue_) {
            new (storage_.data) T(move(*other));
            hasValue_ = true;
            other.reset();
        }
    }

    // Destructor: destroy value if present
    ~SimpleOptional() { reset(); }

    // Assignment
    SimpleOptional& operator=(const T& val) {
        if (hasValue_) {
            **this = val;  // Assign to existing value
        } else {
            new (storage_.data) T(val);  // Construct new value
            hasValue_ = true;
        }
        return *this;
    }

    // Reset: destroy value if present
    void reset() {
        if (hasValue_) {
            get()->~T();    // Explicit destructor
            hasValue_ = false;
        }
    }

    // Emplace: destroy old value (if any) and construct new one in-place
    template<typename... Args>
    T& emplace(Args&&... args) {
        reset();
        T* p = new (storage_.data) T(forward<Args>(args)...);
        hasValue_ = true;
        return *p;
    }

    bool has_value() const { return hasValue_; }
    explicit operator bool() const { return hasValue_; }

    T& operator*()        { return *get(); }
    const T& operator*() const { return *get(); }
    T* operator->()       { return get(); }

private:
    T* get() { return reinterpret_cast<T*>(storage_.data); }
    const T* get() const { return reinterpret_cast<const T*>(storage_.data); }

    struct alignas(T) Storage {
        char data[sizeof(T)];
    } storage_;

    bool hasValue_;
};

struct Config {
    string host;
    int    port;
    bool   tls;

    Config(string h, int p, bool t) : host(h), port(p), tls(t) {
        cout << "  Config(" << host << ":" << port << ") created" << endl;
    }
    Config(const Config& o) : host(o.host), port(o.port), tls(o.tls) {
        cout << "  Config copied" << endl;
    }
    ~Config() {
        cout << "  Config(" << host << ":" << port << ") destroyed" << endl;
    }
};

int main() {
    cout << "=== SimpleOptional demo ===" << endl;
    SimpleOptional<Config> opt;
    cout << "Has value: " << opt.has_value() << endl;

    cout << "\nAssigning Config..." << endl;
    opt = Config("localhost", 8080, false);
    cout << "Has value: " << opt.has_value()
         << "  host=" << opt->host
         << "  port=" << opt->port << endl;

    cout << "\nEmplacing new Config..." << endl;
    opt.emplace("prod.example.com", 443, true);
    cout << "Has value: " << opt.has_value()
         << "  host=" << opt->host
         << "  tls=" << opt->tls << endl;

    cout << "\nResetting..." << endl;
    opt.reset();
    cout << "Has value: " << opt.has_value() << endl;

    cout << "\nOptional going out of scope:" << endl;
    // Destructor: no value, nothing to destroy
    return 0;
}

Output:

Plaintext
=== SimpleOptional demo ===
Has value: 0

Assigning Config...
  Config(localhost:8080) created
  Config copied
  Config(localhost:8080) destroyed
Has value: 1  host=localhost  port=8080

Emplacing new Config...
  Config(localhost:8080) destroyed
  Config(prod.example.com:443) created
Has value: 1  host=prod.example.com  tls=1

Resetting...
  Config(prod.example.com:443) destroyed
Has value: 0

Optional going out of scope:

Step-by-step explanation:

  1. SimpleOptional maintains a Storage struct with alignas(T) and char data[sizeof(T)] — the same aligned buffer pattern. When a value is present, T lives in this buffer, constructed via placement new.
  2. reset() calls get()->~T() if hasValue_ is true — the only time to explicitly call a destructor. After the call, hasValue_ is set to false but the raw bytes in storage_ remain (in indeterminate state).
  3. emplace(args...) first calls reset() (destroying any existing value), then uses placement new to construct a new T directly in the buffer with the provided arguments. No copy or move of a temporary occurs.
  4. The operator= from a T value checks whether a value already exists: if so, it assigns (no construction/destruction); if not, it constructs with placement new. This optimization avoids a needless destroy-and-reconstruct cycle.
  5. This is essentially how std::optional<T> is implemented in every standard library. The same pattern applies to std::variant (which holds one of several types in a union-like buffer) and std::any.

std::launder: The Modern Placement New Companion

C++17 introduced std::launder to address a subtle issue with placement new and pointer provenance that affects optimizing compilers.

C++
#include <new>
#include <cassert>
using namespace std;

struct Immutable {
    const int value;  // const member
    Immutable(int v) : value(v) {}
};

int main() {
    alignas(Immutable) char buf[sizeof(Immutable)];

    Immutable* p1 = new (buf) Immutable(42);
    assert(p1->value == 42);

    // Replace p1 with a new Immutable at the same address
    p1->~Immutable();
    Immutable* p2 = new (buf) Immutable(100);

    // WITHOUT std::launder: the compiler may "know" that *p1
    // had value=42 and optimize p1->value to 42 even now.
    // This is because p1 and p2 point to the same address,
    // but p2 is a new object with potentially different value.

    // WITH std::launder: tells the compiler "I know this pointer
    // now points to a new object — please reread from memory"
    Immutable* p2_laundered = launder(p2);
    assert(p2_laundered->value == 100);  // Correct

    p2->~Immutable();

    return 0;
}

std::launder is needed in very specific scenarios: when you have replaced an object with a const member (or in a const-qualified location) via placement new and want to access the new object through a pointer to the old one. In practice, this arises in std::optional, std::variant, and similar types. For most placement new usage with non-const members (like the object pool and vector above), std::launder is not needed.

Common Mistakes with Placement New

Mistake 1: Using delete instead of explicit destructor + manual deallocation.

C++
char buf[sizeof(T)];
T* p = new (buf) T(args);
delete p;           // WRONG: tries to free buf, which is on the stack
p->~T();            // CORRECT: destroy object
// buf goes out of scope normally — no deallocation needed

Mistake 2: Forgetting to call the destructor.

C++
char* pool = new char[sizeof(T) * N];
T* obj = new (pool) T(args);
delete[] pool;      // WRONG: ~T() was never called — resources leaked
obj->~T();          // CORRECT: call this first
delete[] pool;      // THEN free the raw memory

Mistake 3: Insufficient alignment.

C++
char buf[sizeof(double)];    // align=1, NOT enough for double (align=8)
double* d = new (buf) double(3.14);  // UB on strict-alignment archs

alignas(double) char buf[sizeof(double)];  // CORRECT
double* d = new (buf) double(3.14);

Mistake 4: Accessing an object after placement new without std::launder when needed.

C++
struct WithConst { const int x; };
alignas(WithConst) char buf[sizeof(WithConst)];
WithConst* p = new (buf) WithConst{1};
p->~WithConst();
WithConst* p2 = new (buf) WithConst{2};
// Accessing through p: might get 1 due to optimizer's const assumption
int v = p->x;                          // Potentially wrong
int v2 = std::launder(p2)->x;          // Correct: 2

Mistake 5: Using placement new for arrays without tracking the count.

C++
alignas(T) char buf[sizeof(T) * N];
// This calls array placement new — different from N individual placement news
T* arr = new (buf) T[N];   // Note: may write hidden array-size metadata BEFORE buf
                            // (implementation-defined) — can corrupt adjacent memory
// Better: use N individual placement news
for (int i = 0; i < N; i++) new (buf + i * sizeof(T)) T(args);

Placement New Quick Reference

OperationSyntaxNotes
Construct in placenew (ptr) T(args...)Calls constructor at ptr, no allocation
Destroy manuallyptr->~T()Only valid for placement new objects
Aligned bufferalignas(T) char buf[sizeof(T)]Required for correct alignment
Aligned heap memory::operator new(n * sizeof(T))Returns aligned raw memory
Free raw heap memory::operator delete(ptr)Paired with ::operator new
Find aligned addressstd::align(alignment, size, ptr, space)Adjusts ptr to alignment
Launder pointerstd::launder(ptr)For const members or after replacement
Check alignmentalignof(T)Returns alignment requirement of T

Conclusion

Placement new is the low-level mechanism that separates memory allocation from object construction in C++. By allowing you to call a constructor at a specific address — without allocating any memory — it enables the entire family of advanced memory management techniques: memory pools with O(1) allocation and deallocation, arena allocators that free all objects at once, object recyclers that reuse the same memory slots, and the standard library containers themselves.

The lifecycle rule for placement new is absolute: every object constructed with placement new must eventually have its destructor called explicitly with ptr->~T() before the underlying memory is freed or reused. This is the only place in C++ where you call a destructor directly, and forgetting it leaks resources. Never use delete on a pointer that was constructed with placement new in external memory — delete combines destruction and deallocation; you want only the destruction part.

Alignment is the other critical requirement: always use alignas(T) for stack buffers, ::operator new for heap buffers, or std::align to find aligned positions within existing buffers. Misaligned placement new is undefined behavior — usually silently wrong on x86, reliably crashing on ARM.

Placement new is not something most application code uses directly. But it is what every well-designed memory pool, every std::optional, every std::variant, and every std::vector uses under the hood. Understanding it gives you the mental model to build these abstractions yourself and to understand the behavior of the standard library’s most important types.

Share:
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments

Discover More

Setting Up Your JavaScript Development Environment: Editors and Tools

Learn how to set up your JavaScript development environment, from choosing editors to using tools…

How Dangerous Is It to Work with Robots?

Learn about safety risks in robotics and how to protect yourself. Understand electrical hazards, mechanical…

MIT Technology Review Predicts 2026 Breakthrough Tech Trends

MIT Technology Review reveals 2026’s transformative technologies including AI companions, commercial space stations, and personalized…

Nvidia’s Groq Licensing Play Shows Big Tech’s New M&A Workaround For AI Chips

Nvidia’s Groq licensing deal spotlights how inference performance and deal structures are redefining the AI…

Understanding Op-Amps: The Swiss Army Knife of Analog Electronics

Understanding Op-Amps: The Swiss Army Knife of Analog Electronics

Discover what operational amplifiers are, how they work, and why they’re essential in analog electronics.…

Creating an Impressive Data Science GitHub Repository

Learn how to build a standout GitHub repository for your data science portfolio. Discover best…

Click For More
0
Would love your thoughts, please comment.x
()
x