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:
#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:
=== 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 0x7ffd2c4b3aa0Step-by-step explanation:
new (buffer) Widget(2, "Beta")is placement new syntax. The(buffer)part is the memory address — no heap allocation happens. TheWidget(2, "Beta")part calls the constructor at that address. The return value is the same address cast toWidget*.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 (thestring namemember here) without freeing the memory. If you useddelete w2, the program would attempt to free memory it doesn’t own — undefined behavior.- After
w2->~Widget(), thebuffermemory 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. alignas(Widget) char buffer[sizeof(Widget)]ensures the buffer is aligned toWidget‘s alignment requirement. Withoutalignas, the buffer’s alignment is onlychar(1 byte), which may be insufficient forWidgeton some architectures, causing undefined behavior.- The array example constructs objects at computed offsets:
arr + i * sizeof(Widget). EachWidgetlands 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.
#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:
=== 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 destroyStep-by-step explanation:
alignof(T)returns the alignment requirement of typeTas asize_t. Adoublemust 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.alignas(T) char buffer[sizeof(T)]is the correct idiom for a buffer that can hold aT.alignasensures the buffer starts at an address with at leastalignof(T)alignment. This is so common that the standard providesstd::aligned_storage<sizeof(T), alignof(T)>(deprecated in C++23) and recommends thealignaspattern directly.std::align(alignment, size, ptr, space)finds the next aligned address within a buffer. If the buffer has room for the aligned object, it adjustsptrto the aligned address, reducesspaceby the alignment padding, and returnsptr. Returnsnullptrif there is not enough space.AlignedStorage<T>is a utility that packages the buffer and construction/destruction operations together. The standard library’sstd::optionalandstd::variantuse 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.
#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:
=== 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:
reserve(4)calls::operator new(4 * sizeof(Resource))— raw memory allocation. No constructors are called. The memory holds 4Resource-sized blocks of uninitialized bytes.emplace_back(2)callsnew (data_ + 1) Resource(2)— constructsResourcedirectly at slot 1 with the arguments forwarded. No temporary is created. This is whyemplace_backis more efficient thanpush_backfor complex types.pop_back()callsdata_[2].~Resource()— the destructor runs andResource(3)is cleaned up. The memory at slot 2 is not freed — it remains allocated as part of the vector’s capacity. The nextemplace_backwill reuse it.- The destructor iterates
size_(notcapacity_) and calls the destructor for each live element. Only elements that were actually constructed (via placement new) get their destructors called. The raw memory betweensize_andcapacity_is freed without destructing — correct, because no objects live there. - 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 youreserve, andpop_backis 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.
#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:
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 availableStep-by-step explanation:
construct(1, 10.0f, 20.0f, 100, "Goblin")pops a slot from the free list (O(1)), marks it in-use, and callsnew (slotPtr(slot)) Enemy(...). TheEnemyconstructor runs in place — no heap allocation.destroy(e1)callse1->~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 nextconstructcall.- The
bitset<Capacity> inUse_tracks which slots hold live objects. Theassert(inUse_[slot])indestroycatches double-free bugs at runtime — a common error in manual memory management. - 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. - When enemies are despawned and new ones spawned, the same physical memory slots are reused. The game does not interact with
malloc/freeduring 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.
#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:
=== 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:
SimpleOptionalmaintains aStoragestruct withalignas(T)andchar data[sizeof(T)]— the same aligned buffer pattern. When a value is present,Tlives in this buffer, constructed via placement new.reset()callsget()->~T()ifhasValue_is true — the only time to explicitly call a destructor. After the call,hasValue_is set tofalsebut the raw bytes instorage_remain (in indeterminate state).emplace(args...)first callsreset()(destroying any existing value), then uses placement new to construct a newTdirectly in the buffer with the provided arguments. No copy or move of a temporary occurs.- The
operator=from aTvalue 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. - This is essentially how
std::optional<T>is implemented in every standard library. The same pattern applies tostd::variant(which holds one of several types in a union-like buffer) andstd::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.
#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.
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 neededMistake 2: Forgetting to call the destructor.
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 memoryMistake 3: Insufficient alignment.
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.
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: 2Mistake 5: Using placement new for arrays without tracking the count.
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
| Operation | Syntax | Notes |
|---|---|---|
| Construct in place | new (ptr) T(args...) | Calls constructor at ptr, no allocation |
| Destroy manually | ptr->~T() | Only valid for placement new objects |
| Aligned buffer | alignas(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 address | std::align(alignment, size, ptr, space) | Adjusts ptr to alignment |
| Launder pointer | std::launder(ptr) | For const members or after replacement |
| Check alignment | alignof(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.








