Require types used with construct are trivially destructible

This commit is contained in:
2025-08-14 12:06:42 -04:00
parent ba827a71ce
commit 9d4c08747c
2 changed files with 49 additions and 19 deletions

View File

@@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <new> #include <new>
#include <type_traits>
#include <utility> #include <utility>
/** /**
@@ -36,8 +37,9 @@
* ArenaAllocator arena(1024); * ArenaAllocator arena(1024);
* void* ptr = arena.allocate(100); * void* ptr = arena.allocate(100);
* *
* // Construct objects in-place * // Construct trivially destructible objects in-place
* MyClass* obj = arena.construct<MyClass>(arg1, arg2); * int* num = arena.construct<int>(42);
* MyPOD* obj = arena.construct<MyPOD>(arg1, arg2); // If MyPOD is trivial
* *
* // Track memory usage * // Track memory usage
* size_t total = arena.total_allocated(); * size_t total = arena.total_allocated();
@@ -231,23 +233,36 @@ public:
* This is a convenience method that combines allocation with in-place * This is a convenience method that combines allocation with in-place
* construction. It properly handles alignment requirements for type T. * construction. It properly handles alignment requirements for type T.
* *
* @tparam T The type of object to construct * @tparam T The type of object to construct (must be trivially destructible)
* @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 Pointer to the constructed object
* @throws std::bad_alloc if memory allocation fails * @throws std::bad_alloc if memory allocation 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.
*
* ## Example: * ## Example:
* ```cpp * ```cpp
* std::string* str = arena.construct<std::string>("Hello, World!"); * int* num = arena.construct<int>(42); // ✓ Trivially
* MyClass* obj = arena.construct<MyClass>(arg1, arg2, arg3); * destructible MyPOD* pod = arena.construct<MyPOD>(arg1, arg2); // ✓ If
* MyPOD is trivial std::string* str = arena.construct<std::string>("hi"); //
* ✗ Compile error!
* ``` * ```
* *
* ## Note: * ## Note:
* Objects constructed this way cannot be individually destroyed. * Objects constructed this way cannot be individually destroyed.
* Their destructors will NOT be called automatically. * 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> T *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(sizeof(T), alignof(T)); void *ptr = allocate(sizeof(T), alignof(T));
return new (ptr) T(std::forward<Args>(args)...); return new (ptr) T(std::forward<Args>(args)...);
} }

View File

@@ -1,5 +1,6 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "arena_allocator.hpp" #include "arena_allocator.hpp"
#include <cstring>
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -106,10 +107,17 @@ TEST_CASE("ArenaAllocator construct template") {
CHECK(*ptr == 42); CHECK(*ptr == 42);
} }
SUBCASE("construct string") { SUBCASE("construct array") {
std::string *ptr = arena.construct<std::string>("hello world"); struct FixedString {
char data[16];
FixedString(const char *str) {
std::strncpy(data, str, sizeof(data) - 1);
data[sizeof(data) - 1] = '\0';
}
};
FixedString *ptr = arena.construct<FixedString>("hello world");
CHECK(ptr != nullptr); CHECK(ptr != nullptr);
CHECK(*ptr == "hello world"); CHECK(std::strcmp(ptr->data, "hello world") == 0);
} }
SUBCASE("construct multiple objects") { SUBCASE("construct multiple objects") {
@@ -122,10 +130,15 @@ TEST_CASE("ArenaAllocator construct template") {
} }
SUBCASE("construct with multiple arguments") { SUBCASE("construct with multiple arguments") {
auto *ptr = arena.construct<std::pair<int, std::string>>(42, "test"); struct IntPair {
int first;
int second;
IntPair(int a, int b) : first(a), second(b) {}
};
auto *ptr = arena.construct<IntPair>(42, 24);
CHECK(ptr != nullptr); CHECK(ptr != nullptr);
CHECK(ptr->first == 42); CHECK(ptr->first == 42);
CHECK(ptr->second == "test"); CHECK(ptr->second == 24);
} }
} }
@@ -266,27 +279,29 @@ TEST_CASE("ArenaAllocator edge cases") {
} }
} }
struct TestObject { struct TestPOD {
int value; int value;
std::string name; char name[16];
TestObject(int v, const std::string &n) : value(v), name(n) {} TestPOD(int v, const char *n) : value(v) {
~TestObject() = default; std::strncpy(name, n, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
}
}; };
TEST_CASE("ArenaAllocator with custom objects") { TEST_CASE("ArenaAllocator with custom objects") {
ArenaAllocator arena; ArenaAllocator arena;
TestObject *obj1 = arena.construct<TestObject>(42, "first"); TestPOD *obj1 = arena.construct<TestPOD>(42, "first");
TestObject *obj2 = arena.construct<TestObject>(84, "second"); TestPOD *obj2 = arena.construct<TestPOD>(84, "second");
CHECK(obj1 != nullptr); CHECK(obj1 != nullptr);
CHECK(obj2 != nullptr); CHECK(obj2 != nullptr);
CHECK(obj1 != obj2); CHECK(obj1 != obj2);
CHECK(obj1->value == 42); CHECK(obj1->value == 42);
CHECK(obj1->name == "first"); CHECK(std::strcmp(obj1->name, "first") == 0);
CHECK(obj2->value == 84); CHECK(obj2->value == 84);
CHECK(obj2->name == "second"); CHECK(std::strcmp(obj2->name, "second") == 0);
} }
TEST_CASE("ArenaAllocator geometric growth policy") { TEST_CASE("ArenaAllocator geometric growth policy") {