如何简化向基类shared_ptr容器存储派生类对象的save函数实现
问题分析
你当前实现的冗余完全来自不必要的运行时类型检查:
- 你的容器元素类型本身就是
std::shared_ptr<Base>,所有派生类的std::shared_ptr都可以隐式转换为基类智能指针,根本不需要通过std::dynamic_pointer_cast转换后再存储,转换操作对存储逻辑没有任何实际意义。 - 你把
save函数的参数定义为std::shared_ptr<Base>,主动丢弃了传入参数的静态类型信息,才不得不依赖运行时RTTI做类型判断,既增加性能开销,又违反开闭原则,新增派生类必须修改save函数逻辑。 - 原实现中
emplace使用std::piecewise_construct的写法完全多余,直接传入键和值即可。
最优解决方案(零运行时开销,编译期类型检查)
如果调用save时传入的是静态类型明确的派生类智能指针(你给出的示例代码就是这种场景),直接用模板+编译期类型约束实现即可,完全不需要任何运行时类型判断,性能拉满。
核心思路是保留传入参数的静态派生类类型,在编译期完成类型校验和日志匹配,存储时直接隐式转换为基类智能指针。
实现代码(兼容C17,C11可通过模板特化适配)
#include <iostream> #include <string> #include <memory> #include <map> #include <type_traits> class Base { public: Base() {} virtual ~Base() {} }; class Class1 : public Base { public: Class1() : Base() {} ~Class1() {} }; class Class2 : public Base { public: Class2() : Base() {} ~Class2() {} }; class Class3 : public Base { public: Class3() : Base() {} ~Class3() {} }; std::map<std::string, std::shared_ptr<Base>> myContainer; // 编译期匹配类型对应的日志 template<typename T> constexpr const char* get_save_log() { if constexpr (std::is_same_v<T, Class1>) { return "save obj1"; } else if constexpr (std::is_same_v<T, Class2>) { return "save obj2"; } else if constexpr (std::is_same_v<T, Class3>) { return "save obj3"; } else { // 传入不支持的派生类直接编译报错,避免运行时静默失败 static_assert(!sizeof(T), "Unsupported derived class of Base"); } } // 仅允许Base的派生类传入,编译期完成类型校验 template<typename Derived, typename = std::enable_if_t<std::is_base_of_v<Base, Derived>>> void save(const std::string& id, std::shared_ptr<Derived> obj) { std::cout << get_save_log<Derived>() << std::endl; // 直接存入,派生类智能指针隐式转为基类智能指针,零开销 myContainer.emplace(id, std::move(obj)); } int main() { std::shared_ptr<Class1> a1 = std::make_shared<Class1>(); std::shared_ptr<Class2> a2 = std::make_shared<Class2>(); std::shared_ptr<Class3> a3 = std::make_shared<Class3>(); save("id1", a1); save("id2", a2); save("id3", a3); std::cout << "size is " << myContainer.size() << std::endl; return 0; }
方案优势
- 无任何RTTI、
dynamic_cast开销,类型判断全部在编译期完成,性能和原生代码一致 - 类型安全:传入非Base派生类、未支持的派生类会直接编译报错,不会出现原实现中漏判导致的静默存储失败问题
- 易扩展:新增派生类时,只需要在
get_save_log中新增一个分支即可,不需要修改save核心逻辑
其他可选方案
方案1:多态虚函数(适合必须接收基类指针的场景)
如果你的业务场景中save函数确实只能拿到擦除了静态类型的std::shared_ptr<Base>,可以把类型相关的逻辑下沉到虚函数中,性能远好于逐个dynamic_cast判断。
实现方式是在基类中定义虚接口,每个派生类实现接口返回日志字符串:
class Base { public: Base() {} virtual ~Base() {} virtual const char* get_save_log() const = 0; }; class Class1 : public Base { public: const char* get_save_log() const override { return "save obj1"; } // 其余代码不变 }; // Class2、Class3同理实现get_save_log void save(const std::string& id, std::shared_ptr<Base> obj) { if (!obj) return; std::cout << obj->get_save_log() << std::endl; myContainer.emplace(id, std::move(obj)); }
这个方案的运行时开销只有一次虚表寻址,是固定开销,和派生类数量无关,比随派生类数量线性增长的dynamic_cast方案性能高很多。
方案2:std::variant(适合类型集合固定的场景)
如果你所有可能存储的派生类在编译期完全确定,不需要开放扩展,可以用std::variant替代基类多态,完全不需要虚函数和RTTI,性能和模板方案一致。
注意std::tuple不适合该场景,tuple是固定大小的异质集合,无法实现动态键值对的类型擦除存储。
variant实现示例:
// 定义所有允许存储的类型 using Element = std::variant<std::shared_ptr<Class1>, std::shared_ptr<Class2>, std::shared_ptr<Class3>>; std::map<std::string, Element> myContainer; template<typename T> void save(const std::string& id, std::shared_ptr<T> obj) { static_assert(std::is_constructible_v<Element, std::shared_ptr<T>>, "Unsupported type"); if constexpr (std::is_same_v<T, Class1>) { std::cout << "save obj1" << std::endl; } else if constexpr (std::is_same_v<T, Class2>) { std::cout << "save obj2" << std::endl; } else if constexpr (std::is_same_v<T, Class3>) { std::cout << "save obj3" << std::endl; } myContainer.emplace(id, std::move(obj)); }
这个方案的限制是类型集合封闭,新增类型必须修改Element的variant定义,适合类型完全固定的业务场景。
性能对比
| 方案 | 运行时开销 | 扩展性 | 类型安全 |
|---|---|---|---|
| 模板编译期方案 | 零 | 好 | 编译期检查 |
| std::variant方案 | 零 | 一般(类型封闭) | 编译期检查 |
| 虚函数多态方案 | 一次虚函数调用(固定开销) | 好 | 运行时判空 |
| 原dynamic_cast遍历方案 | N次RTTI检查(随派生类数线性增长) | 差 | 易漏判 |
内容的提问来源于stack exchange,提问作者Pasha
相关产品推荐
相关产品推荐

