/** * @file FreeList.h * @brief 带槽位复用机制的稀疏对象容器模板 * * @details * free_list 是一种特殊的容器,维护"已使用槽位 + 空闲链表"结构, * 特别适合需要频繁增删元素且使用索引作为外部句柄的场景。 * * ## 核心特性 * * - **O(1) 增删操作**:新增和删除操作近似常数时间复杂度 * - **槽位复用**:删除后的槽位会被回收并重新利用,避免频繁内存分配 * - **索引句柄**:使用 u32 索引作为外部句柄,与 id 系统完美配合 * - **内存紧凑**:底层使用连续内存,缓存友好 * * ## 数据结构原理 * * ``` * 初始状态: * _array: [ 空 | 空 | 空 | 空 ] * _next_free_index = invalid_id * _size = 0 * * 添加元素 A、B、C 后: * _array: [ A | B | C | 空 ] * _next_free_index = invalid_id * _size = 3 * * 删除元素 B 后(槽位 1 被标记为空闲): * _array: [ A | ->2 | C | 空 ] // 槽位 1 存储下一个空闲索引 2 * _next_free_index = 1 // 指向第一个空闲槽位 * _size = 2 * * 再删除元素 A 后: * _array: [ ->1 | ->2 | C | 空 ] // 槽位 0 存储下一个空闲索引 1 * _next_free_index = 0 * _size = 1 * * 添加新元素 D 时(复用槽位 0): * _array: [ D | ->2 | C | 空 ] * _next_free_index = 1 // 指向下一个空闲槽位 * _size = 2 * ``` * * ## 空闲链表机制 * * 删除元素时,该槽位被加入空闲链表: * 1. 调用元素的析构函数 * 2. 将当前 `_next_free_index` 值写入该槽位(作为链表下一个节点) * 3. 更新 `_next_free_index` 指向该槽位 * * 添加元素时,优先从空闲链表获取槽位: * 1. 如果 `_next_free_index` 有效,复用该槽位 * 2. 从槽位中读取下一个空闲索引,更新 `_next_free_index` * 3. 否则,在数组末尾追加新元素 * * ## 类型要求 * * 由于空闲槽位需要存储 u32 索引,要求: * - `sizeof(T) >= sizeof(u32)`(至少 4 字节) * * ## 使用场景 * * - 游戏对象管理(实体 ID 系统) * - 资源句柄管理(纹理、网格等) * - 渲染表面管理(多窗口场景) * - 任何需要稳定索引句柄的系统 * * ## 注意事项 * * - 删除后的索引可能被新元素复用,外部系统需要处理这种情况 * - 不保证元素在内存中的顺序 * - 与 std::vector 配合使用时可能触发重复构造(见 USE_STL_VECTOR 宏) * * @example * @code * utl::free_list objects; * * // 添加对象 * u32 id1 = objects.add(player_data); * u32 id2 = objects.add(enemy_data); * * // 访问对象 * objects[id1].update(); * * // 删除对象(槽位被回收) * objects.remove(id1); * * // 新对象可能复用 id1 的槽位 * u32 id3 = objects.add(new_data); // id3 可能等于 id1 * @endcode * * @see utl::vector * @see DEFINE_TYPED_ID */ #pragma once #include "CommonHeader.h" namespace XEngine::utl { #if USE_STL_VECTOR #pragma message("WARNING: using utl::free_list with std::vector result in duplicate calls to class constructor!") #endif /** * @class free_list * @brief 带槽位复用机制的稀疏对象容器 * * @tparam T 元素类型,必须满足 `sizeof(T) >= sizeof(u32)` * * @details * 该容器使用空闲链表管理已删除的槽位,实现 O(1) 的增删操作。 * 特别适合需要稳定索引句柄的场景,如游戏对象管理、资源句柄等。 */ template class free_list { static_assert(sizeof(T) >= sizeof(u32), "Type T must be at least 4 bytes to store free list indices"); public: /** * @brief 默认构造函数,创建空容器 * * @details * 创建一个空的 free_list,不预分配任何内存。 * 首次添加元素时会触发内存分配。 */ free_list() = default; /** * @brief 预留容量的构造函数 * * @param count 预留的元素槽位数量 * * @details * 预分配指定数量的槽位,避免后续添加元素时的多次扩容。 * 适用于已知大概元素数量的场景。 */ explicit free_list(u32 count) { _array.reserve(count); } /** * @brief 析构函数,销毁容器 * * @details * 断言检查容器为空(_size == 0),确保所有元素已被正确移除。 * 在 DEBUG 模式下使用 USE_STL_VECTOR 时,会清空内存以便检测悬空引用。 */ ~free_list() { assert(!_size); #if USE_STL_VECTOR memset(_array.data(), 0, _array.size() * sizeof(T)); #endif } /** * @brief 添加新元素并返回其槽位 ID * * @tparam params 构造参数类型包 * @param p 传递给元素构造函数的参数 * @return 新分配的槽位 ID(u32 索引) * * @details * 添加元素的策略: * 1. **有空闲槽位**:从空闲链表头部取出一个槽位复用 * 2. **无空闲槽位**:在数组末尾追加新元素 * * 复用槽位时: * - 从该槽位读取下一个空闲索引,更新 `_next_free_index` * - 使用 placement new 在该槽位构造新元素 * * @note 返回的 ID 在元素被删除前保持有效 * @note 删除后该 ID 可能被新元素复用 */ template constexpr u32 add(params&&... p) { u32 id{ u32_invalid_id }; if (_next_free_index == u32_invalid_id) { id = (u32)_array.size(); _array.emplace_back(std::forward(p)...); } else { id = _next_free_index; assert(id < _array.size() && already_removed(id)); _next_free_index = *(const u32 *const)std::addressof(_array[id]); new (std::addressof(_array[id])) T(std::forward(p)...); } ++_size; return id; } /** * @brief 移除指定 ID 的元素并回收其槽位 * * @param id 要移除元素的槽位 ID * * @details * 移除元素的步骤: * 1. 调用元素的析构函数 * 2. DEBUG 模式下填充 0xcc 标记已删除 * 3. 将当前 `_next_free_index` 写入该槽位(链表链接) * 4. 更新 `_next_free_index` 指向该槽位 * * @pre id 必须是有效的已分配槽位 * @pre 该槽位未被删除(不能重复删除) */ constexpr void remove(u32 id) { assert(id < _array.size() && !already_removed(id)); T& item{ _array[id] }; item.~T(); DEBUG_OP(memset(std::addressof(_array[id]), 0xcc, sizeof(T))); *(u32 *const)std::addressof(_array[id]) = _next_free_index; _next_free_index = id; --_size; } /** * @brief 获取当前有效元素数量 * * @return 容器中有效元素的个数 * * @note 这与 capacity() 不同,size() 返回实际存储的元素数 */ constexpr u32 size() const { return _size; } /** * @brief 获取已分配槽位总数 * * @return 底层数组的大小(包括空闲槽位) * * @details * 返回值 >= size(),因为可能存在已删除但未释放的槽位。 * 这些槽位会在后续添加元素时被复用。 */ constexpr u32 capacity() const { return _array.size(); } /** * @brief 检查容器是否为空 * * @return 如果容器中没有有效元素则返回 true */ constexpr bool empty() const { return _size == 0; } /** * @brief 通过 ID 访问元素(可修改) * * @param id 元素的槽位 ID * @return 元素的引用 * * @pre id 必须有效且指向未删除的元素 */ [[nodiscard]] constexpr T& operator[](u32 id) { assert(id < _array.size() && !already_removed(id)); return _array[id]; } /** * @brief 通过 ID 访问元素(只读) * * @param id 元素的槽位 ID * @return 元素的常量引用 * * @pre id 必须有效且指向未删除的元素 */ [[nodiscard]] constexpr const T& operator[](u32 id) const { assert(id < _array.size() && !already_removed(id)); return _array[id]; } private: /** * @brief 检查指定槽位是否已被删除 * * @param id 要检查的槽位 ID * @return 如果槽位已被删除则返回 true * * @details * 通过检查槽位内容是否被 0xcc 填充来判断是否已删除。 * 仅在 sizeof(T) > sizeof(u32) 时有效,否则直接返回 true。 * * @note 仅用于断言检查,确保不会访问已删除的元素 */ constexpr bool already_removed(u32 id) const { if constexpr (sizeof(T) > sizeof(u32)) { u32 i{ sizeof(u32) }; const u8 *const p{ (const u8 *const)std::addressof(_array[id]) }; while ((p[i] == 0xcc) && (i < sizeof(T))) ++i; return i == sizeof(T); } else { return true; } } #if USE_STL_VECTOR utl::vector _array; #else utl::vector _array; #endif u32 _next_free_index{ u32_invalid_id }; u32 _size{ 0 }; }; }