如何将std::tuple转为std::any并用作map键实现游戏资源缓存?
问题1:是否可以将任意参数直接转换为std::any?
不行,存在两个核心限制:
- std::any仅支持存储可拷贝构造的实值类型,不能存储引用类型、不可拷贝类型。你代码中使用
std::forward_as_tuple打包的参数tuple里包含数组引用const char (&)[78]、右值引用,都不符合std::any的存储要求,因此直接触发转换错误。 - 即使你将参数转为实值存入std::any,std::any本身没有默认的
<比较运算符,无法直接作为std::map的键,这是后续会遇到的第二个编译问题。
问题2:如何将任意构造参数作为std::map的键使用?
要实现该需求,需要解决两个核心问题:一是把参数打包为支持比较的实值类型,二是对参数类型做语义适配,保证相同语义的参数(比如字符串字面量和std::string)能被识别为同一个键。
具体实现步骤
- 参数按值存储并做语义转换
不要用std::forward_as_tuple(会保留引用),改用std::make_tuple存储参数的实值,同时对特殊类型做适配:比如把C风格字符串自动转为std::string,避免比较指针地址而不是字符串内容。 - 实现类型擦除的可比较键
自己封装一个键类型,用类型擦除隐藏参数的具体类型,同时保留比较逻辑:
class KeyBase { public: virtual ~KeyBase() = default; virtual bool less(const KeyBase& other) const = 0; virtual std::unique_ptr<KeyBase> clone() const = 0; }; template<typename T> class KeyImpl : public KeyBase { public: explicit KeyImpl(T val) : value(std::move(val)) {} bool less(const KeyBase& other) const override { const auto* other_ptr = dynamic_cast<const KeyImpl<T>*>(&other); if (!other_ptr) return typeid(T).before(typeid(other)); return value < other_ptr->value; } std::unique_ptr<KeyBase> clone() const override { return std::make_unique<KeyImpl<T>>(*this); } private: T value; }; class ErasedKey { public: template<typename T> ErasedKey(T val) : impl_(std::make_unique<KeyImpl<T>>(std::move(val))) {} ErasedKey(const ErasedKey& other) : impl_(other.impl_->clone()) {} ErasedKey(ErasedKey&& other) noexcept = default; ErasedKey& operator=(const ErasedKey& other) { impl_ = other.impl_->clone(); return *this; } ErasedKey& operator=(ErasedKey&& other) noexcept = default; bool operator<(const ErasedKey& other) const { return impl_->less(*other.impl_); } private: std::unique_ptr<KeyBase> impl_; };
- 修正缓存逻辑
把map的键改成std::pair<std::type_index, ErasedKey>,同时修正对象构造的逻辑:
class SingletonFactory { public: template<typename T, typename... Args> const T& get(Args&&... args) { // 对参数做decay和语义适配,C风格字符串转std::string auto processed_args = std::make_tuple( []<typename U>(U&& arg) { if constexpr (std::is_convertible_v<U, std::string> && !std::is_same_v<std::decay_t<U>, std::string>) { return std::string(std::forward<U>(arg)); } else { return std::forward<U>(arg); } }(std::forward<Args>(args))... ); auto key = std::make_pair(std::type_index(typeid(T)), ErasedKey(processed_args)); auto it = _cache.find(key); if (it != _cache.end()) { return std::any_cast<const T&>(it->second); } // 构造新对象存入缓存 T& obj = std::any_cast<T&>(_cache.emplace( std::move(key), T(std::forward<Args>(args)...) ).first->second); return obj; } void clear() { _cache.clear(); } private: std::map<std::pair<std::type_index, ErasedKey>, std::any> _cache{}; };
如果用到自定义类型作为构造参数,只要为该类型实现operator<比较运算符即可正常工作。
适配你的需求特性说明
- 同类型同参数的资源会自动复用,避免重复加载
- 所有资源存在工厂的map里,工厂销毁时会自动调用所有资源的析构函数,满足确定性释放的要求
- 不需要手动追踪资源实例,调用
get即可自动获取或创建资源
内容的提问来源于stack exchange,提问作者mpen
相关产品推荐
相关产品推荐

