移除std::function开销:ECS框架TypedView模板优化问询
核心原理
ecs.hpp框架消除std::function开销的核心思路是将类型擦除从运行期转移到编译期:通过模板元编程直接绑定用户的处理逻辑,利用编译器的参数推导和代码生成能力,避免std::function带来的虚调用、堆分配等运行时开销。
具体来说,它放弃了std::function的类型擦除机制,转而用模板化的方法接受任意可调用对象(lambda、函数指针、仿函数),在编译期直接生成适配当前可调用对象的遍历代码,完全不需要运行时的类型检查或间接调用。
简化实现代码
以下是一个最小化的可运行示例,核心实现TypedView的each方法,完全避免std::function:
#include <unordered_map> #include <vector> #include <cstdint> #include <utility> #include <tuple> #include <cstdio> // 定义示例组件 struct Transform { float x, y; }; struct Velocity { float dx, dy; }; // 实体管理器:负责实体创建和组件存储 class EntityManager { public: using EntityId = uint32_t; EntityId create_entity() { return next_entity_id++; } // 给实体添加组件 template<typename Comp> void add_component(EntityId e, Comp&& comp) { get_component_map<Comp>()[e] = std::forward<Comp>(comp); } // 获取实体的指定组件 template<typename Comp> Comp& get_component(EntityId e) { return get_component_map<Comp>()[e]; } // 检查实体是否拥有指定组件 template<typename Comp> bool has_component(EntityId e) { auto& map = get_component_map<Comp>(); return map.find(e) != map.end(); } // 获取拥有所有指定组件的实体列表 template<typename... Comps> std::vector<EntityId> get_matching_entities() { std::vector<EntityId> result; if constexpr (sizeof...(Comps) > 0) { // 取第一个组件的实体集合作为基础,过滤其他组件 using FirstComp = std::tuple_element_t<0, std::tuple<Comps...>>; auto& first_map = get_component_map<FirstComp>(); for (auto& [entity, _] : first_map) { // C++17折叠表达式:检查实体是否拥有所有组件 if ((has_component<Comps>(entity) && ...)) { result.push_back(entity); } } } return result; } private: EntityId next_entity_id = 1; // 每个组件类型对应一个独立的存储容器(静态局部变量,确保单例) template<typename Comp> static std::unordered_map<EntityId, Comp>& get_component_map() { static std::unordered_map<EntityId, Comp> component_map; return component_map; } }; // TypedView:筛选并处理拥有指定组件的实体 template<typename... Comps> class TypedView { public: explicit TypedView(EntityManager& em) : em_(em) {} // 核心方法:接受任意可调用对象,编译期直接生成调用逻辑 template<typename Func> void each(Func&& func) { auto entities = em_.get_matching_entities<Comps...>(); for (EntityId e : entities) { // 编译期展开组件参数,直接传递给处理函数 func(em_.get_component<Comps>(e)...); } } private: EntityManager& em_; }; // 辅助函数:简化TypedView的创建 template<typename... Comps> TypedView<Comps...> view(EntityManager& em) { return TypedView<Comps...>(em); } // 使用示例 int main() { EntityManager em; // 创建实体并添加组件 auto e1 = em.create_entity(); em.add_component(e1, Transform{10.f, 20.f}); em.add_component(e1, Velocity{1.f, 0.5f}); auto e2 = em.create_entity(); em.add_component(e2, Transform{5.f, 5.f}); em.add_component(e2, Velocity{-0.5f, 1.f}); // 调用each处理实体,直接传入lambda,无std::function开销 view<Transform, Velocity>(em).each([](Transform& t, Velocity& v) { t.x += v.dx; t.y += v.dy; printf("Entity position: (%.1f, %.1f)\n", t.x, t.y); }); return 0; }
关键细节解析
模板化的each方法
each方法的参数Func是模板类型,编译器会根据传入的可调用对象(比如示例中的lambda)推导其具体类型,直接生成适配该类型的调用代码,完全没有运行时的类型擦除操作。编译期参数展开
利用C++17的折叠表达式(has_component<Comps>(entity) && ...)和模板参数展开func(em_.get_component<Comps>(e)...),在编译期处理任意数量的组件类型,不需要运行时的类型检查或动态参数传递。组件存储的独立性
每个组件类型对应独立的std::unordered_map,符合ECS的数据导向设计,缓存友好,同时避免了面向对象的封装带来的数据碎片化。对比entityx的方案
entityx用std::function做运行期类型擦除,是为了让TypedView的each方法可以接受不同类型的可调用对象,但代价是运行时开销;而本方案通过编译期模板推导,既保留了接受任意可调用对象的灵活性,又完全消除了std::function的开销。
内容的提问来源于stack exchange,提问作者user540983

