storage, no object live object UB or dangling

What you write:

T* p = new T(42);p->use();delete p;

What the compiler does with it (simplified):

void* mem = ::operator new(sizeof(T));T* p = ::new (mem) T(42);p->use();p->~T();::operator delete(mem);

The shaded gap between the two traces is the whole story. Placement new works in the left gap (storage exists, no object yet). Pools and std::vector recycle the right gap (object destroyed, storage kept).

1Where storage comes from

Every object sits inside storage with one of four durations. Drag through a program run and watch which objects are alive, which have storage but no object, and which are gone.

Note the dynamic Order: it is created inside the block but survives the block, because heap storage ignores scope. That is both its purpose and the reason it can leak.

2Four primitive operations

Every expression that touches an object is some combination of allocate, construct, destroy, and deallocate. new and delete bundle two each; the rest let you do one at a time.

Expression
alloc

ctor

dtor

free
new T(args)✓✓
delete p✓✓
::operator new(n)
alloc.allocate(n)
✓
new (buf) T(args)
std::construct_at(p, args)
✓
std::destroy_at(p)
p->~T()
✓
::operator delete(p)
alloc.deallocate(p, n)
✓
T x(args); (local)all four, automatically at scope entry and exit

Now run them yourself. The block below is one T-sized slot. Some sequences are legal, some leak, and some are undefined behaviour. Try to break it.

Start with either new T(1) or ::operator new.

3Construction and destruction order

The language guarantees this sequence: bases first, then members in declaration order, then the constructor body. Destruction is the exact reverse. The order you write in the initializer list is ignored (compilers warn with -Wreorder).

struct D : B {
    M a;
    N b;
    D() : b(), a() {}  // a still first
};
void f() {
    D d;
}   // d destroyed here

    Predict before you flip the toggle: if b's constructor throws, which destructors run? The rule that falls out: an object's destructor only runs if its constructor finished, because its lifetime never began otherwise.

    4Dangles or fine?

    Each snippet uses something after a line of code. Decide whether the object is still alive at the use, then watch its lifetime play out. The bar is the object; the diamond is the use.

    5Inside std::vector

    A vector owns raw capacity and constructs elements into it one slot at a time. Growth is where every idea so far collides: allocation, construction, moved-from objects that are still alive, destruction, and exception safety.

    buffer
    beginendend of capacity moved-from, still alive

      Things to try: push five elements with noexcept on and notice the old slots turn into moved-from objects that still need destroying. Then turn noexcept off, enable the throwing copy, reset, and push five again. The vector copies instead of moving so that when the copy throws, the original buffer is untouched. That is the strong exception guarantee, and it is why your move constructors should be noexcept.

      The new element is constructed before the old ones are transferred, matching libstdc++. One reason: in v.push_back(v[0]) the argument refers into the old buffer, which must stay intact until the copy is made.

      6Counting allocations

      delete p only calls operator delete after the destructor has already run. If you replace the global operator new and operator delete, every allocation in the program passes through your code:

      delete p;
      →
      p->~T();lifetime ends
      →
      operator delete(p);your replacement
      →
      std::free(p);or a pool, or anything
      std::atomic<std::size_t> g_allocs{0};
      
      void* operator new(std::size_t sz) {
          ++g_allocs;
          void* p = std::malloc(sz ? sz : 1);
          if (!p) throw std::bad_alloc{};
          return p;
      }
      void operator delete(void* p) noexcept {
          std::free(p);
      }

      Predict: 100 calls to push_back on an empty std::vector<int> (libstdc++, capacity doubles). How far does g_allocs move?

      7A pool: lifetime without allocation

      A matching engine can't afford malloc latency on every order. Instead it allocates all its slots once at startup, then only constructs and destroys on the hot path. Free slots hold a pointer to the next free slot, so acquiring and releasing are both O(1) pointer swaps.

      union Slot {
          Order order;   // while in use
          Slot* next;    // while free
          Slot() : next(nullptr) {}
          ~Slot() {}
      };
      
      // hot path: no allocation
      template <class... Args>
      Order* acquire(Args&&... a) {
          Slot* s = head;
          head = s->next;
          return std::construct_at(&s->order,
              std::forward<Args>(a)...);
      }
      void release(Order* o) {
          std::destroy_at(o);
          // a union member shares its address
          auto* s = reinterpret_cast<Slot*>(o);
          s->next = head;
          head = s;
      }
      Tap a live order to cancel it.
      Pool built at startup: one allocation for all 8 slots. Every slot starts on the free list.

      Released slots go to the front of the list, so the most recently freed slot is reused first. That slot is likely still warm in cache, which is a second latency win on top of skipping the allocator.

      8Check yourself

      Answer out loud before opening each one.