You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

游戏资源管理器改用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 16:27:03