diff --git a/src/arena_allocator.hpp b/src/arena_allocator.hpp index 460415d..d428329 100644 --- a/src/arena_allocator.hpp +++ b/src/arena_allocator.hpp @@ -302,38 +302,87 @@ public: 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 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. * - * This is a convenience method that combines allocation with in-place - * construction. It properly handles alignment requirements for type T. + * This method returns different types based on whether T is trivially + * destructible: + * - For trivially destructible types: returns T* (raw pointer) + * - For non-trivially destructible types: returns ArenaAllocator::Ptr + * (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 * @param args Arguments to forward to T's constructor - * @return Pointer to the constructed object + * @return T* for trivially destructible types, ArenaAllocator::Ptr + * otherwise * @note Prints error to stderr and calls std::abort() if memory allocation * fails - * - * ## Type Requirements: - * T must be trivially destructible (std::is_trivially_destructible_v). - * 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 T *construct(Args &&...args) { - static_assert( - std::is_trivially_destructible_v, - "ArenaAllocator::construct requires trivially destructible types. " - "Objects constructed in the arena will not have their destructors " - "called."); + template auto construct(Args &&...args) { void *ptr = allocate_raw(sizeof(T), alignof(T)); - return new (ptr) T(std::forward(args)...); + T *obj = new (ptr) T(std::forward(args)...); + + if constexpr (std::is_trivially_destructible_v) { + return obj; + } else { + return Ptr(obj); + } } /** diff --git a/tests/test_arena_allocator.cpp b/tests/test_arena_allocator.cpp index dd0b7fc..fcddfea 100644 --- a/tests/test_arena_allocator.cpp +++ b/tests/test_arena_allocator.cpp @@ -598,3 +598,162 @@ TEST_CASE("format function fallback codepath") { 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(42); + static_assert(std::is_same_v, + "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(42); + static_assert( + std::is_same_v>, + "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(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(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(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(ptr) == true); + } + + SUBCASE("ArenaAllocator::Ptr reset functionality") { + ArenaAllocator arena; + + auto ptr = arena.construct(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(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(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(42); + auto ptr2 = arena.construct(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 + } +}