如何以异常安全的方式使用placement new?MyStack类实现分析
Great question! Let's start by breaking down what's wrong with your current Push() method, then walk through how to fix it for exception safety—plus some other critical issues you might have missed.
Push() Right now, you're updating the _top pointer before constructing the object with placement new. If T() throws an exception during construction (say, T's constructor allocates memory and fails), here's what happens:
_topis already pointing to a new position, but no valid object exists at the oldprevaddress.- Your stack's internal state is corrupted:
Empty()will return false even though there's no valid object, and future calls toPop()will access unconstructed memory—this is undefined behavior.
This breaks the basic exception safety guarantee: if an exception is thrown, your object should stay in a consistent state with no resources leaked.
Push() for Exception Safety The fix is straightforward: construct the object first, then update the stack's state only if construction succeeds. Placement new doesn't allocate memory (you already handled that with Reserve()), but the object's constructor can still throw. We need to make sure the stack's pointers don't change unless the object is fully built.
Here's the corrected, exception-safe version:
template <typename T> T* Push() { Reserve(sizeof(T)); // Grab the target address without touching _top yet char* const new_obj_addr = _top; // If construction throws, we exit immediately—_top stays untouched new (new_obj_addr) T(); // Only update _top if the object was successfully constructed _top += sizeof(T); return reinterpret_cast<T*>(new_obj_addr); }
You don't even need a try/catch block here! If the placement new throws, the function exits before _top is modified, leaving the stack in its original state. Perfectly safe.
Reserve()Must Also Be Exception-Safe
YourReserve()function handles buffer reallocation. For full safety, it needs to follow the "commit or rollback" rule too:- Allocate the new buffer first (if allocation throws, the original stack stays intact).
- Move/copy existing objects to the new buffer. If any move/copy throws, destroy the objects already moved, free the new buffer, and leave the original stack alone.
- Only after all objects are safely moved, destroy the old objects, free the old buffer, and update
_bottomand_top.
Your
Pop()Has a Big Bug
Right now,Pop()just moves the_toppointer but never calls the object's destructor. This causes resource leaks (if T manages files, memory, etc.) and undefined behavior when the buffer is reused. A correct, exception-safePop()(assuming you want to destroy the popped object) looks like this:template <typename T> void Pop() { if (Empty()) { throw std::underflow_error("Can't pop from empty stack"); } _top -= sizeof(T); T* popped_obj = reinterpret_cast<T*>(_top); // Destructors should not throw (per C++ best practices) popped_obj->~T(); }If T's destructor does throw (which it shouldn't), the standard calls
std::terminate—so stick to non-throwing destructors for types you use in this stack.Alignment is a Hidden Hazard
Usingchar*for your buffer pointers can cause alignment issues:Tmight require stricter alignment thancharprovides, leading to undefined behavior. Fix this by allocating memory with proper alignment, e.g.:// Allocate buffer aligned for any standard type _bottom = static_cast<char*>(operator new(buffer_size, std::align_val_t(alignof(std::max_align_t))));For types with custom alignment, you'll need to track the required alignment and adjust
Reserve()accordingly.
For exception-safe placement new in container code like this:
- Never modify the container's state before the object is fully constructed.
- Make sure all helper functions (like
Reserve()) follow the same exception safety rules. - Don't forget about destructors and alignment—these are critical for correctness, not just exception safety.
内容的提问来源于stack exchange,提问作者Shmoopy

