Very useful, many thanks for the heads-up.
Something to point out is that the problem is not the destructor by itself as much as it is the combination of an owning raw pointer plus compiler-generated memberwise copying. A destructor still allows implicit copy operations, but prevents implicit move operation.
Also, In copy assignment, saying the destructor does not run is.. technically true, but slightly imprecise: assignment never invokes the destination object's destructor. The bug is that its currently owned buffer is overwritten without an explicit buffer_delete, creating a leak before the eventual double-return/corruption.
The analysis is accurate. After the same pooled buffer is deleted twice:
pool[pi] = b;
b->next = b;
The first later buffer_new() returns b while leaving pool[pi] still pointing at it, meaning it resets b->next to nullptr and the second buffer_new() then returns that exact same b again. So two apparently separate owners receive one allocation.
The LS result in the mock is expected, but it is not directly detecting the double-return. x and y are raw pointers that are never passed to buffer_delete; because both refer to the same allocation, LS reports one buffer object plus its 8192-byte payload as leaked. Simply adding assert(x != y) or something similiar makes the allocator corruption visible immediately.
Mock: [Hidden Content]
For a prevention-only fix, deleting copies and implementing moves is the right choice:
TEMP_BUFFER(const TEMP_BUFFER&) = delete;
TEMP_BUFFER& operator=(const TEMP_BUFFER&) = delete;
TEMP_BUFFER(TEMP_BUFFER&& other) noexcept
: buf(std::exchange(other.buf, nullptr))
, forceDelete(std::exchange(other.forceDelete, false))
{
}
TEMP_BUFFER& operator=(TEMP_BUFFER&& other) noexcept
{
if (this != &other)
{
buffer_delete(buf);
buf = std::exchange(other.buf, nullptr);
forceDelete = std::exchange(other.forceDelete, false);
}
return *this;
}
but the deep-copy assignment example you proposed has one meaningful weakness: it deletes the current buffer before allocating and filling the replacement. If allocation or buffer_write can fail, buf may remain dangling or the object may be left partially modified.
A copy-and-swap approach is preferable:
void swap(TEMP_BUFFER& other) noexcept
{
using std::swap;
swap(buf, other.buf);
swap(forceDelete, other.forceDelete);
}
TEMP_BUFFER& operator=(const TEMP_BUFFER& other)
{
if (this != &other)
{
TEMP_BUFFER copy(other);
swap(copy);
}
return *this;
}
That is only safe if the copy constructor itself correctly handles allocation/write failure according to the real buffer_new and buffer_write contracts, and the deep-copy constructor must preserve every semantically relevant buffer field.
Copying only write_point_pos is fine only if TEMP_BUFFER intentionally represents a fresh writable snapshot. If it can wrap a partly-read packet, it should also preserve read position and any other state that affects interpretation.
If TEMP_BUFFER has unique ownership and no meaningful use case where you have to duplicate a buffer, making it move-only is the better design imo.
struct TEMP_BUFFER
{
LPBUFFER buf{nullptr};
bool forceDelete{false};
TEMP_BUFFER(const TEMP_BUFFER&) = delete;
TEMP_BUFFER& operator=(const TEMP_BUFFER&) = delete;
TEMP_BUFFER(TEMP_BUFFER&& other) noexcept
: buf(std::exchange(other.buf, nullptr))
, forceDelete(std::exchange(other.forceDelete, false))
{
}
TEMP_BUFFER& operator=(TEMP_BUFFER&& other) noexcept
{
if (this != &other)
{
buffer_delete(buf);
buf = std::exchange(other.buf, nullptr);
forceDelete = std::exchange(other.forceDelete, false);
}
return *this;
}
~TEMP_BUFFER()
{
buffer_delete(buf);
}
};
Copy semantics are only required where code genuinely needs an independent duplicate, for example:
TEMP_BUFFER b = a;
Passing an lvalue TEMP_BUFFER by value.
Storing it in an API or container operation that copies elements.
Returning or assigning from an lvalue where a move is not requested.
Normal return-by-value code does not usually require copying because NRVO or move construction handles it. Standard containers can also hold move-only types, provided the element is movable and preferably noexcept movable.