无需继承非模板基类,如何在非模板类中存储任意模板类?
不用非模板基类存储任意模板类的几种方法
当然有啦!除了你现在用的继承非模板基类的方式,C++里还有不少不用基类就能存储任意模板类实例的方案,我给你逐个拆解:
1. 使用std::any(C++17+)
std::any是C++标准库提供的类型安全的任意类型容器,它可以存储任意类型的对象,完全不需要依赖基类继承。
示例代码:
#include <any> #include <iostream> template <typename... Types> class ExtendedA { public: void print() const { std::cout << "ExtendedA with " << sizeof...(Types) << " template args\n"; } }; class Storage { public: std::any toStore; }; int main() { Storage a; // 存储不同模板实例的对象 a.toStore = ExtendedA<int, char>(); a.toStore = ExtendedA<double>(); // 取出对象时需要明确类型,用std::any_cast if (auto* ptr = std::any_cast<ExtendedA<double>>(&a.toStore)) { ptr->print(); // 输出: ExtendedA with 1 template args } }
优点:
- 完全灵活,能存任何类型,不用提前规划类型范围
- 标准库原生支持,无需手动实现复杂逻辑
缺点:
- 取出时必须知道具体类型,否则会抛出异常(或返回空指针)
- 相比继承方式,有一定的类型擦除开销
2. 使用std::variant(C++17+)
如果你能提前确定要存储的模板实例类型范围,std::variant会是更类型安全、更高效的选择——它是一个类型安全的联合体,只能存储预先指定的类型集合。
示例代码:
#include <variant> #include <iostream> #include <string> template <typename... Types> class ExtendedA { public: void print() const { std::cout << "ExtendedA with " << sizeof...(Types) << " template args\n"; } }; // 预先定义允许存储的模板实例类型 using ExtendedAVariant = std::variant<ExtendedA<int>, ExtendedA<char, std::string>, ExtendedA<double>>; class Storage { public: ExtendedAVariant toStore; }; int main() { Storage a; a.toStore = ExtendedA<char, std::string>(); // 用std::visit访问variant中的对象 std::visit([](const auto& obj) { obj.print(); // 输出: ExtendedA with 2 template args }, a.toStore); }
优点:
- 类型安全,编译时就能检查非法类型
- 性能比
std::any好,无额外堆分配(除非存储的类型本身需要) - 访问对象时无需手动判断类型,
std::visit会自动分发
缺点:
- 必须提前枚举所有可能存储的模板实例类型,灵活性不如
std::any
3. 手动实现无基类的类型擦除
如果你的项目还不能用C++17,或者需要更底层的控制,可以手动实现类型擦除——用void*存储对象指针,再搭配函数指针封装对象的核心操作(比如销毁、复制、调用方法等),完全不需要继承基类。
示例代码:
#include <iostream> template <typename... Types> class ExtendedA { public: void print() const { std::cout << "ExtendedA with " << sizeof...(Types) << " template args\n"; } }; class Storage { private: void* objPtr = nullptr; // 封装销毁对象的函数指针 void (*destroyFn)(void*) = nullptr; // 封装print操作的函数指针 void (*printFn)(const void*) = nullptr; public: // 模板构造函数,接受任意类型的对象 template <typename T> Storage(T&& obj) { objPtr = new T(std::forward<T>(obj)); destroyFn = [](void* ptr) { delete static_cast<T*>(ptr); }; printFn = [](const void* ptr) { static_cast<const T*>(ptr)->print(); }; } // 禁止拷贝(如需拷贝可添加拷贝构造和赋值,封装拷贝函数指针) Storage(const Storage&) = delete; Storage& operator=(const Storage&) = delete; // 移动构造/赋值 Storage(Storage&& other) noexcept : objPtr(other.objPtr), destroyFn(other.destroyFn), printFn(other.printFn) { other.objPtr = nullptr; other.destroyFn = nullptr; other.printFn = nullptr; } Storage& operator=(Storage&& other) noexcept { if (this != &other) { if (destroyFn) destroyFn(objPtr); objPtr = other.objPtr; destroyFn = other.destroyFn; printFn = other.printFn; other.objPtr = nullptr; other.destroyFn = nullptr; other.printFn = nullptr; } return *this; } ~Storage() { if (destroyFn) destroyFn(objPtr); } // 对外暴露操作接口 void print() const { if (printFn) printFn(objPtr); } }; int main() { Storage a = ExtendedA<int, char>(); a.print(); // 输出: ExtendedA with 2 template args Storage b = ExtendedA<double>(); b.print(); // 输出: ExtendedA with 1 template args }
优点:
- 不依赖C++17特性,兼容旧版本编译器
- 完全自定义操作逻辑,开销可控
- 无需修改原模板类的结构
缺点:
- 需要手动管理内存和函数指针,代码量较大
- 逻辑复杂,容易出现内存泄漏或类型错误
对比你原来的继承方案
你原来的继承基类方案,本质是利用C++多态实现类型擦除,优点是符合面向对象设计习惯,缺点是必须给基类添加虚函数、所有派生类都要遵循基类接口,且虚函数调用有轻微运行时开销。上面的几种方案则摆脱了基类约束,各有适用场景——如果追求灵活选std::any,追求类型安全选std::variant,兼容旧环境则选手动类型擦除。
内容的提问来源于stack exchange,提问作者Donut
相关产品推荐
相关产品推荐

