Add ArenaAllocator::Ptr
This commit is contained in:
@@ -302,38 +302,87 @@ public:
|
|||||||
new_size * sizeof(T), alignof(T)));
|
new_size * sizeof(T), alignof(T)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Smart pointer for arena-allocated objects with non-trivial
|
||||||
|
* destructors.
|
||||||
|
*
|
||||||
|
* ArenaAllocator::Ptr calls the destructor but does not free memory (assumes
|
||||||
|
* arena allocation). This provides RAII semantics for objects that need
|
||||||
|
* cleanup without the overhead of individual memory deallocation.
|
||||||
|
*
|
||||||
|
* @tparam T The type of object being managed
|
||||||
|
*/
|
||||||
|
template <typename T> struct Ptr {
|
||||||
|
Ptr() noexcept : ptr_(nullptr) {}
|
||||||
|
|
||||||
|
explicit Ptr(T *ptr) noexcept : ptr_(ptr) {}
|
||||||
|
|
||||||
|
Ptr(const Ptr &) = delete;
|
||||||
|
Ptr &operator=(const Ptr &) = delete;
|
||||||
|
|
||||||
|
Ptr(Ptr &&other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; }
|
||||||
|
|
||||||
|
Ptr &operator=(Ptr &&other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
reset();
|
||||||
|
ptr_ = other.ptr_;
|
||||||
|
other.ptr_ = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
~Ptr() { reset(); }
|
||||||
|
|
||||||
|
T *operator->() const noexcept { return ptr_; }
|
||||||
|
T &operator*() const noexcept { return *ptr_; }
|
||||||
|
|
||||||
|
T *get() const noexcept { return ptr_; }
|
||||||
|
|
||||||
|
explicit operator bool() const noexcept { return ptr_ != nullptr; }
|
||||||
|
|
||||||
|
T *release() noexcept {
|
||||||
|
T *result = ptr_;
|
||||||
|
ptr_ = nullptr;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset(T *new_ptr = nullptr) noexcept {
|
||||||
|
if (ptr_) {
|
||||||
|
ptr_->~T();
|
||||||
|
}
|
||||||
|
ptr_ = new_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
T *ptr_;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Construct an object of type T in the arena using placement new.
|
* @brief Construct an object of type T in the arena using placement new.
|
||||||
*
|
*
|
||||||
* This is a convenience method that combines allocation with in-place
|
* This method returns different types based on whether T is trivially
|
||||||
* construction. It properly handles alignment requirements for type T.
|
* destructible:
|
||||||
|
* - For trivially destructible types: returns T* (raw pointer)
|
||||||
|
* - For non-trivially destructible types: returns ArenaAllocator::Ptr<T>
|
||||||
|
* (smart pointer that calls destructor)
|
||||||
*
|
*
|
||||||
* @tparam T The type of object to construct (must be trivially destructible)
|
* @tparam T The type of object to construct
|
||||||
* @tparam Args Types of constructor arguments
|
* @tparam Args Types of constructor arguments
|
||||||
* @param args Arguments to forward to T's constructor
|
* @param args Arguments to forward to T's constructor
|
||||||
* @return Pointer to the constructed object
|
* @return T* for trivially destructible types, ArenaAllocator::Ptr<T>
|
||||||
|
* otherwise
|
||||||
* @note Prints error to stderr and calls std::abort() if memory allocation
|
* @note Prints error to stderr and calls std::abort() if memory allocation
|
||||||
* fails
|
* fails
|
||||||
*
|
|
||||||
* ## Type Requirements:
|
|
||||||
* T must be trivially destructible (std::is_trivially_destructible_v<T>).
|
|
||||||
* This prevents subtle bugs since destructors are never called for objects
|
|
||||||
* constructed in the arena.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* ## Note:
|
|
||||||
* Objects constructed this way cannot be individually destroyed.
|
|
||||||
* Their destructors will NOT be called automatically - hence the requirement
|
|
||||||
* for trivially destructible types.
|
|
||||||
*/
|
*/
|
||||||
template <typename T, typename... Args> T *construct(Args &&...args) {
|
template <typename T, typename... Args> auto construct(Args &&...args) {
|
||||||
static_assert(
|
|
||||||
std::is_trivially_destructible_v<T>,
|
|
||||||
"ArenaAllocator::construct requires trivially destructible types. "
|
|
||||||
"Objects constructed in the arena will not have their destructors "
|
|
||||||
"called.");
|
|
||||||
void *ptr = allocate_raw(sizeof(T), alignof(T));
|
void *ptr = allocate_raw(sizeof(T), alignof(T));
|
||||||
return new (ptr) T(std::forward<Args>(args)...);
|
T *obj = new (ptr) T(std::forward<Args>(args)...);
|
||||||
|
|
||||||
|
if constexpr (std::is_trivially_destructible_v<T>) {
|
||||||
|
return obj;
|
||||||
|
} else {
|
||||||
|
return Ptr<T>(obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -598,3 +598,162 @@ TEST_CASE("format function fallback codepath") {
|
|||||||
CHECK(result == "Valid format: 42");
|
CHECK(result == "Valid format: 42");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test object with non-trivial destructor for ArenaAllocator::Ptr testing
|
||||||
|
class TestObject {
|
||||||
|
public:
|
||||||
|
static int destructor_count;
|
||||||
|
static int constructor_count;
|
||||||
|
|
||||||
|
int value;
|
||||||
|
|
||||||
|
TestObject(int v) : value(v) { constructor_count++; }
|
||||||
|
|
||||||
|
~TestObject() { destructor_count++; }
|
||||||
|
|
||||||
|
static void reset_counters() {
|
||||||
|
constructor_count = 0;
|
||||||
|
destructor_count = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int TestObject::destructor_count = 0;
|
||||||
|
int TestObject::constructor_count = 0;
|
||||||
|
|
||||||
|
// Test struct with trivial destructor
|
||||||
|
struct TrivialObject {
|
||||||
|
int value;
|
||||||
|
TrivialObject(int v) : value(v) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("ArenaAllocator::Ptr smart pointer functionality") {
|
||||||
|
TestObject::reset_counters();
|
||||||
|
|
||||||
|
SUBCASE("construct returns raw pointer for trivially destructible types") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
auto ptr = arena.construct<TrivialObject>(42);
|
||||||
|
static_assert(std::is_same_v<decltype(ptr), TrivialObject *>,
|
||||||
|
"construct() should return raw pointer for trivially "
|
||||||
|
"destructible types");
|
||||||
|
CHECK(ptr != nullptr);
|
||||||
|
CHECK(ptr->value == 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCASE("construct returns ArenaAllocator::Ptr for non-trivially "
|
||||||
|
"destructible types") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
auto ptr = arena.construct<TestObject>(42);
|
||||||
|
static_assert(
|
||||||
|
std::is_same_v<decltype(ptr), ArenaAllocator::Ptr<TestObject>>,
|
||||||
|
"construct() should return ArenaAllocator::Ptr for non-trivially "
|
||||||
|
"destructible types");
|
||||||
|
CHECK(ptr);
|
||||||
|
CHECK(ptr->value == 42);
|
||||||
|
CHECK(TestObject::constructor_count == 1);
|
||||||
|
CHECK(TestObject::destructor_count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCASE("ArenaAllocator::Ptr calls destructor on destruction") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto ptr = arena.construct<TestObject>(42);
|
||||||
|
CHECK(TestObject::constructor_count == 1);
|
||||||
|
CHECK(TestObject::destructor_count == 0);
|
||||||
|
} // ptr goes out of scope
|
||||||
|
|
||||||
|
CHECK(TestObject::destructor_count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCASE("ArenaAllocator::Ptr move semantics") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
auto ptr1 = arena.construct<TestObject>(42);
|
||||||
|
CHECK(TestObject::constructor_count == 1);
|
||||||
|
|
||||||
|
auto ptr2 = std::move(ptr1);
|
||||||
|
CHECK(!ptr1); // ptr1 should be null after move
|
||||||
|
CHECK(ptr2);
|
||||||
|
CHECK(ptr2->value == 42);
|
||||||
|
CHECK(TestObject::destructor_count == 0); // No destruction yet
|
||||||
|
|
||||||
|
ptr2.reset();
|
||||||
|
CHECK(TestObject::destructor_count == 1); // Destructor called
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCASE("ArenaAllocator::Ptr access operators") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
auto ptr = arena.construct<TestObject>(123);
|
||||||
|
|
||||||
|
// Test operator->
|
||||||
|
CHECK(ptr->value == 123);
|
||||||
|
|
||||||
|
// Test operator*
|
||||||
|
CHECK((*ptr).value == 123);
|
||||||
|
|
||||||
|
// Test get()
|
||||||
|
TestObject *raw_ptr = ptr.get();
|
||||||
|
CHECK(raw_ptr != nullptr);
|
||||||
|
CHECK(raw_ptr->value == 123);
|
||||||
|
|
||||||
|
// Test bool conversion
|
||||||
|
CHECK(ptr);
|
||||||
|
CHECK(static_cast<bool>(ptr) == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCASE("ArenaAllocator::Ptr reset functionality") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
auto ptr = arena.construct<TestObject>(42);
|
||||||
|
CHECK(TestObject::constructor_count == 1);
|
||||||
|
CHECK(TestObject::destructor_count == 0);
|
||||||
|
|
||||||
|
ptr.reset();
|
||||||
|
CHECK(!ptr);
|
||||||
|
CHECK(TestObject::destructor_count == 1);
|
||||||
|
|
||||||
|
// Reset with new object
|
||||||
|
TestObject *raw_obj = arena.construct<TestObject>(84).release();
|
||||||
|
ptr.reset(raw_obj);
|
||||||
|
CHECK(ptr);
|
||||||
|
CHECK(ptr->value == 84);
|
||||||
|
CHECK(TestObject::constructor_count == 2);
|
||||||
|
CHECK(TestObject::destructor_count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCASE("ArenaAllocator::Ptr release functionality") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
auto ptr = arena.construct<TestObject>(42);
|
||||||
|
TestObject *raw_ptr = ptr.release();
|
||||||
|
|
||||||
|
CHECK(!ptr); // ptr should be null after release
|
||||||
|
CHECK(raw_ptr != nullptr);
|
||||||
|
CHECK(raw_ptr->value == 42);
|
||||||
|
CHECK(TestObject::destructor_count == 0); // No destructor called
|
||||||
|
|
||||||
|
// Manually call destructor (since we released ownership)
|
||||||
|
raw_ptr->~TestObject();
|
||||||
|
CHECK(TestObject::destructor_count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SUBCASE("ArenaAllocator::Ptr move assignment") {
|
||||||
|
ArenaAllocator arena;
|
||||||
|
|
||||||
|
auto ptr1 = arena.construct<TestObject>(42);
|
||||||
|
auto ptr2 = arena.construct<TestObject>(84);
|
||||||
|
|
||||||
|
CHECK(TestObject::constructor_count == 2);
|
||||||
|
CHECK(TestObject::destructor_count == 0);
|
||||||
|
|
||||||
|
ptr1 = std::move(ptr2); // Should destroy first object, move second
|
||||||
|
|
||||||
|
CHECK(!ptr2); // ptr2 should be null
|
||||||
|
CHECK(ptr1);
|
||||||
|
CHECK(ptr1->value == 84);
|
||||||
|
CHECK(TestObject::destructor_count == 1); // First object destroyed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user