C++ ECS实现中返回派生类型shared_ptr向量引用问题
问题根源
你当前的组件存储用std::type_index做键,统一存储std::vector<std::shared_ptr<IEComponent>>类型的组件列表,存在两个天然的类型适配问题:
- C++中
std::vector<std::shared_ptr<派生类>>和std::vector<std::shared_ptr<基类>>是完全独立的类型,不存在隐式转换,无法直接把基类vector当派生类vector使用 - 如果逐元素做指针转换后拷贝生成新的派生类vector,操作的是独立的vector副本,对新vector做的增删、元素替换操作不会同步回原存储;但实际上你通过副本里的shared_ptr修改组件实例本身的成员是会生效的,只是vector层面的操作不同步,且每次调用都拷贝vector会产生无意义的性能开销。
实现方案
不需要修改现有存储结构,通过轻量的视图包装类实现零拷贝的类型适配,遍历访问时实时做静态指针转换即可——由于组件存入容器时已经通过typeid(T)严格匹配类型,不需要运行时动态类型检查,无额外开销。
首先补全你现有代码中已经调用的基类vector获取接口:
std::vector<std::shared_ptr<IEComponent>>& GetBaseComponents(std::type_index type_idx) { return components_map.at(type_idx); }
实现组件视图包装类,绑定原存储vector的引用,适配范围for遍历的迭代器接口:
// 约束:所有传入的组件类型T必须公开继承自IEComponent template<typename T> class ComponentView { private: std::vector<std::shared_ptr<IEComponent>>& target_vec; public: ComponentView(std::vector<std::shared_ptr<IEComponent>>& vec) : target_vec(vec) {} // 范围for所需的迭代器实现 class Iterator { private: std::vector<std::shared_ptr<IEComponent>>::iterator base_iter; public: Iterator(decltype(base_iter) it) : base_iter(it) {} // 解引用时直接做静态指针转换,无运行时检查开销 std::shared_ptr<T> operator*() { return std::static_pointer_cast<T>(*base_iter); } Iterator& operator++() { base_iter++; return *this; } bool operator!=(const Iterator& other) const { return base_iter != other.base_iter; } }; Iterator begin() { return Iterator(target_vec.begin()); } Iterator end() { return Iterator(target_vec.end()); } // 补充常用vector操作接口,直接透传到原存储 size_t size() const { return target_vec.size(); } std::shared_ptr<T> operator[](size_t idx) { return std::static_pointer_cast<T>(target_vec[idx]); } void Erase(size_t idx) { if (idx < target_vec.size()) { target_vec.erase(target_vec.begin() + idx); } } };
最后实现你需要的GetComponents<T>()模板接口:
template<typename T> ComponentView<T> GetComponents() { std::type_index type_idx(typeid(T)); // 不存在对应类型组件列表时自动创建空vector,和CreateComponent逻辑对齐 if (components_map.find(type_idx) == components_map.end()) { components_map.emplace(type_idx, std::vector<std::shared_ptr<IEComponent>>()); } return ComponentView<T>(GetBaseComponents(type_idx)); }
使用效果
完全匹配你预期的调用写法,不需要修改上层业务代码:
for (std::shared_ptr<ECPosition> pos : ComponentManager::GetComponents<ECPosition>()) { TraceLog(LOG_INFO, "x is: %f y is: %f", pos->x, pos->y); // 直接修改组件成员、通过视图调用Erase删除组件等操作,都会直接作用在原存储上,改动永久生效 pos->x += 1.0f; }
内容的提问来源于stack exchange,提问作者Nicola Picarella
相关产品推荐
相关产品推荐

