游戏资源管理器改用unique_ptr实现的技术咨询
游戏资源管理器改用unique_ptr的实现方案
问题背景
我是编程新手,正在为游戏实现一款资源管理器。需要保留资源插入顺序(比如tilemap图层要按顺序渲染,不能打乱),同时支持按名称快速获取资源。原本的实现是在resource类中同时用unordered_map和vector存储shared_ptr,功能正常,但觉得不够高效直观。现在想改用unique_ptr实现,知道unique_ptr不能有多个副本,考虑在容器中存储原始指针,求具体实现方案和技术建议。
原实现代码
template<typename T> using umap_str = std::unordered_map<std::string, std::shared_ptr<T>>; template<typename T> using vec_sptr = std::vector<std::shared_ptr<T>>; template<typename T> struct resource { inline static std::size_t _max_id; // 用inline避免类外初始化 umap_str<T> _map; vec_sptr<T> _vec; }; template<typename... RsrcTs> struct resource_manager { template<typename T> using rsrc_t = resource<T>; std::tuple<rsrc_t<RsrcTs>...> storage; template<typename RsrcT> inline vec_sptr<RsrcT>& get_vec() {return std::get<rsrc_t<RsrcT>>(storage)._vec;}; template<typename RsrcT> inline umap_str<RsrcT>& get_map() {return std::get<rsrc_t<RsrcT>>(storage)._map;}; template<typename RsrcT> inline std::size_t& get_maxid() {return std::get<rsrc_t<RsrcT>>(storage)._max_id;}; template<typename RsrcT> std::optional<std::shared_ptr<RsrcT>> get(std::string&& name) { auto& to_get = get_map<RsrcT>(); if (!to_get.contains(name)) return std::nullopt; else return to_get.at(name); }; // 无参添加资源,自动生成名称或使用自定义名称 template<typename RsrcT> std::shared_ptr<RsrcT> add(std::string&& custom_name = "") { static_assert(contains<RsrcT, RsrcTs...>().value, "资源类型未在资源管理器声明中"); std::shared_ptr<RsrcT> new_resource = std::make_shared<RsrcT>(); if (custom_name.empty()) get_map<RsrcT>().insert({std::to_string(get_maxid<RsrcT>()), new_resource}); else { if (get_map<RsrcT>().contains(custom_name)) { std::cout << custom_name << "已被占用,改用ID作为名称\n"; get_map<RsrcT>().insert({std::to_string(get_maxid<RsrcT>()), new_resource}); } else get_map<RsrcT>().insert({custom_name, new_resource}); }; get_vec<RsrcT>().push_back(new_resource); get_maxid<RsrcT>()++; return new_resource; }; // 添加已有的shared_ptr资源 template<typename RsrcT> std::shared_ptr<RsrcT> add(std::shared_ptr<RsrcT> arg, std::string&& custom_name = "") { static_assert(contains<RsrcT, RsrcTs...>().value, "资源类型未在资源管理器声明中"); if (custom_name.empty()) get_map<RsrcT>().insert({std::to_string(get_maxid<RsrcT>()), arg}); else { if (get_map<RsrcT>().contains(custom_name)) { std::cout << custom_name << "已被占用,改用ID作为名称\n"; get_map<RsrcT>().insert({std::to_string(get_maxid<RsrcT>()), arg}); } else get_map<RsrcT>().insert({custom_name, arg}); }; get_vec<RsrcT>().push_back(arg); get_maxid<RsrcT>()++; return arg; }; };
改用unique_ptr的实现方案
核心思路:让vector持有unique_ptr(唯一所有权),unordered_map存储资源在vector中的索引(而非原始指针),既保证资源顺序,又支持快速查找,同时避免悬垂指针问题。
修改后的代码
#include <memory> #include <unordered_map> #include <vector> #include <optional> #include <tuple> #include <string> #include <iostream> // 假设contains元函数已实现,用于检查类型是否在参数包中 template<typename T, typename... Ts> struct contains : std::disjunction<std::is_same<T, Ts>...> {}; template<typename T> using vec_uptr = std::vector<std::unique_ptr<T>>; template<typename T> using umap_str_idx = std::unordered_map<std::string, size_t>; template<typename T> struct resource { inline static std::size_t _max_id; // 用inline避免类外初始化 umap_str_idx<T> _map; // 名称 -> vector索引的映射 vec_uptr<T> _vec; // 持有资源唯一所有权的容器 }; template<typename... RsrcTs> struct resource_manager { template<typename T> using rsrc_t = resource<T>; std::tuple<rsrc_t<RsrcTs>...> storage; template<typename RsrcT> inline vec_uptr<RsrcT>& get_vec() { return std::get<rsrc_t<RsrcT>>(storage)._vec; } template<typename RsrcT> inline umap_str_idx<RsrcT>& get_map() { return std::get<rsrc_t<RsrcT>>(storage)._map; } template<typename RsrcT> inline std::size_t& get_maxid() { return std::get<rsrc_t<RsrcT>>(storage)._max_id; } // 按名称获取资源,返回原始指针的optional template<typename RsrcT> std::optional<RsrcT*> get(std::string&& name) { auto& map = get_map<RsrcT>(); auto it = map.find(name); if (it == map.end()) return std::nullopt; size_t idx = it->second; auto& vec = get_vec<RsrcT>(); // 防御性检查:避免索引越界 if (idx >= vec.size()) return std::nullopt; return vec[idx].get(); } // 创建新资源并添加,返回原始指针 template<typename RsrcT> RsrcT* add(std::string&& custom_name = "") { static_assert(contains<RsrcT, RsrcTs...>::value, "资源类型未在资源管理器声明中"); auto new_resource = std::make_unique<RsrcT>(); RsrcT* raw_ptr = new_resource.get(); std::string key; if (custom_name.empty()) { key = std::to_string(get_maxid<RsrcT>()); } else { if (get_map<RsrcT>().contains(custom_name)) { std::cout << custom_name << "已被占用,改用ID作为名称\n"; key = std::to_string(get_maxid<RsrcT>()); } else { key = std::move(custom_name); } } // 先插入映射(此时vector的size就是新元素的索引) get_map<RsrcT>().emplace(std::move(key), get_vec<RsrcT>().size()); // 将unique_ptr移动到vector中 get_vec<RsrcT>().push_back(std::move(new_resource)); get_maxid<RsrcT>()++; return raw_ptr; } // 添加外部传入的unique_ptr资源,返回原始指针 template<typename RsrcT> RsrcT* add(std::unique_ptr<RsrcT> arg, std::string&& custom_name = "") { static_assert(contains<RsrcT, RsrcTs...>::value, "资源类型未在资源管理器声明中"); if (!arg) return nullptr; // 空指针检查 RsrcT* raw_ptr = arg.get(); std::string key; if (custom_name.empty()) { key = std::to_string(get_maxid<RsrcT>()); } else { if (get_map<RsrcT>().contains(custom_name)) { std::cout << custom_name << "已被占用,改用ID作为名称\n"; key = std::to_string(get_maxid<RsrcT>()); } else { key = std::move(custom_name); } } get_map<RsrcT>().emplace(std::move(key), get_vec<RsrcT>().size()); get_vec<RsrcT>().push_back(std::move(arg)); get_maxid<RsrcT>()++; return raw_ptr; } // 可选:添加资源删除函数 template<typename RsrcT> bool remove(std::string&& name) { auto& map = get_map<RsrcT>(); auto it = map.find(name); if (it == map.end()) return false; size_t idx = it->second; auto& vec = get_vec<RsrcT>(); if (idx >= vec.size()) { map.erase(it); return false; } // 从vector中删除资源 vec.erase(vec.begin() + idx); // 删除映射 map.erase(it); // 更新所有大于当前索引的映射条目(因为vector元素前移,索引减1) for (auto& [key, current_idx] : map) { if (current_idx > idx) { current_idx--; } } return true; } };
关键技术建议
- 明确所有权边界:
vector是资源的唯一所有者,所有资源的创建、销毁都由vector控制,外部仅能通过原始指针访问资源,避免所有权混乱。 - 避免悬垂指针:
- 不要让外部代码长期持有返回的原始指针,确保资源管理器的生命周期长于所有使用资源的模块。
- 删除资源时必须调用
remove函数,同时更新map和vector,避免无效索引导致的访问错误。
- 索引优于原始指针:用索引映射替代原始指针,即使
vector扩容(元素内存移动),索引依然有效,彻底避免悬垂指针问题。 - 增强错误反馈:原代码仅通过控制台打印名称冲突信息,可以改为返回
std::pair<RsrcT*, bool>,其中bool标记是否使用了默认ID,方便上层逻辑处理。 - 线程安全考虑:如果游戏是多线程环境,需为
resource_manager的所有操作添加互斥锁(如std::mutex),防止并发读写导致的竞态条件。 - 防御性检查:在
get和remove函数中添加索引越界检查,避免因意外操作导致的程序崩溃。
内容的提问来源于stack exchange,提问作者Minh Nguyen
相关产品推荐
相关产品推荐

