C++17下std::unique_ptr结合多态的类型转换与对象移动问题
咱们团队现在用C17开发一款卡牌对战游戏,之前受限于跨平台兼容性、库依赖这些问题,一直只能用C03和C11混编,连智能指针都没怎么好好用。现在终于升级到C17了,想用上现代C++的玩法,结果在多态配合std::unique_ptr做类型转换+所有权转移这块卡壳了,想请教下怎么解决这个问题。
先给你唠唠咱们的模型设计:
- 游戏里的所有实体(比如玩家化身、卡牌、怪物卡、法术卡、标记物)都有对应的类,比如
CardInstance是所有卡牌的基类,MonsterCardInstance和SpellCardInstance是它的子类;还有个纯虚接口AbilityHealthInterface,PlayerAvatarInstance和MonsterCardInstance都实现了它。 - 为了严格保证对象的所有权唯一(避免同一个对象被多个游戏区域引用导致的bug),我们用
std::unique_ptr来管理每个实体的实例,各个游戏区域(比如牌库、弃牌堆、市场、战场、活跃法术区)用不同的容器存这些unique_ptr,比如牌库是std::vector<std::unique_ptr<CardInstance>>,战场是std::array<std::unique_ptr<MonsterCardInstance>, 5>,通过std::move来转移对象的所有权,确保一个对象只能在一个区域里存在。
现在要实现的核心功能是:玩家从市场买卡,买的卡如果是怪物卡,就要移到自己的战场空位;如果是法术卡,就移到活跃法术区并触发它的效果。但问题来了——市场里存的是std::unique_ptr<CardInstance>(基类指针),而战场和活跃区存的是子类的unique_ptr,我们想做类似std::dynamic_pointer_cast(给shared_ptr用的动态转换)的操作,但不知道unique_ptr对应的怎么弄,现在的示例代码里这块是空白的,就是想问问怎么实现这种「动态转换+所有权移动」的操作?
咱们现在的示例代码大概是这样的(精简过):
bool BuyCard(size_t idx) { // 找到战场的空位,尝试把市场的卡转成怪物卡移过去 auto newPos = std::find_if(Field.begin(), Field.end(), [&mktref](std::unique_ptr<MonsterCardInstance>& fldref ) { if (fldref == nullptr) { // 这里需要类似dynamic_cast的unique_ptr转换,还要用std::move转移所有权 fldref = /* 这里不知道怎么写dynamic_cast对应的unique_ptr转换 */(std::move(mktref)); return fldref != nullptr; // 转换成功就占用这个空位 } return false; }); if (newPos != Field.end()) { return true; // 怪物卡购买成功 } else { // 不是怪物卡,尝试转成法术卡移到活跃区 Active.emplace_back(/* 同样需要转换+移动 */(std::move(mktref))); if (Active.back() != nullptr) { Active.back()->ActivateSpell(); // 触发法术效果 return true; } } return false; // 购买失败 }
解决方案:自己实现一个dynamic_unique_ptr_cast
C++17的标准库确实没有给unique_ptr提供类似std::dynamic_pointer_cast的工具,但我们可以自己实现一个,原理其实很简单——先对原始指针做dynamic_cast,成功的话就把基类unique_ptr的所有权转移到子类unique_ptr里,失败就返回空的unique_ptr。
给你写个可用的实现:
#include <memory> #include <type_traits> // 针对unique_ptr的动态转换函数,支持基类到子类的转换,转移所有权 template<typename Derived, typename Base> std::unique_ptr<Derived> dynamic_unique_ptr_cast(std::unique_ptr<Base>&& base_ptr) { // 先尝试动态转换原始指针 if (Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr.get())) { // 让基类unique_ptr放弃所有权(release()会返回原始指针,且不会delete) base_ptr.release(); // 用转换后的子类指针构造新的unique_ptr,获得所有权 return std::unique_ptr<Derived>(derived_ptr); } // 转换失败,返回空的unique_ptr,原base_ptr仍然持有所有权 return nullptr; }
怎么在你的代码里用这个函数?
把上面的函数加到你的工具类或者头文件里,然后修改BuyCard函数的逻辑:
bool BuyCard(size_t idx) { // 先把市场里的卡移出来,避免操作原容器时的所有权问题 auto card_from_market = std::move(Market[idx]); // 先尝试转成怪物卡 if (auto monster_card = dynamic_unique_ptr_cast<MonsterCardInstance>(std::move(card_from_market))) { // 找战场的空位 auto free_spot = std::find_if(Field.begin(), Field.end(), [](const auto& spot) { return !spot; }); if (free_spot != Field.end()) { *free_spot = std::move(monster_card); return true; // 怪物卡成功部署到战场 } else { // 战场满了的话,把卡放回市场(可选,根据你的游戏规则调整) Market[idx] = std::move(monster_card); return false; } } else { // 转换怪物卡失败,尝试转成法术卡 // 注意:这里card_from_market还持有所有权,因为刚才转换失败了 if (auto spell_card = dynamic_unique_ptr_cast<SpellCardInstance>(std::move(card_from_market))) { Active.emplace_back(std::move(spell_card)); Active.back()->ActivateSpell(); return true; // 法术卡成功激活 } } // 既不是怪物也不是法术,把卡放回市场 Market[idx] = std::move(card_from_market); return false; }
几个要注意的点
- 必须用右值引用传递
unique_ptr:因为我们要转移所有权,unique_ptr是不可复制的,只能通过移动语义转移,所以函数参数用std::unique_ptr<Base>&&(右值引用)。 - 转换成功后一定要调用
release():如果不调用,基类的unique_ptr在析构时会删除对象,而我们新创建的子类unique_ptr也会管理同一个对象,就会导致双重释放的内存错误。release()会让基类unique_ptr放弃对原始指针的所有权,不会触发delete。 - 转换失败时原
unique_ptr仍持有所有权:这时候你可以继续尝试其他类型的转换,或者把对象放回原来的容器里,不会丢失对象。 - 替代方案:用虚函数判断类型:如果觉得
dynamic_cast性能有点顾虑(其实游戏里这种场景基本可以忽略),也可以给CardInstance加一个纯虚函数,比如virtual CardType GetCardType() const = 0;,子类返回对应的类型枚举,然后用static_cast转换。但这种方式需要维护类型枚举,以后加新的卡牌类型容易漏改,安全性不如dynamic_cast。
这样改完之后,就能完美实现你想要的「基类unique_ptr转子类unique_ptr+所有权移动」的需求,同时严格保证每个游戏对象的所有权唯一,符合你们之前的设计初衷。
内容来源于stack exchange

