如何在已知类型数组中规避C++虚表指针开销?
假设我们有如下动物类的多态实现:
struct animal { virtual ~animal() {} virtual std::string get_noise() const = 0; }; struct dog : public animal { std::string get_noise() const override { return "Woof"; } }; struct cat : public animal { std::string get_noise() const override { return "Meow"; } };
通过基类可以写出通用函数:
void make_noise(const animal& target) { std::cout << target.get_noise() << std::endl; }
还有根据输入类型返回实例的工厂函数:
std::unique_ptr<animal> make_animal(const std::string& type) { if (type == "dog") { return std::unique_ptr<animal>(new dog); } else { return std::unique_ptr<animal>(new cat); } }
但当需要创建10亿只猫的数组时,问题出现了:
std::vector<cat> cats(10000000000);
因为继承带来的虚表指针,cat的大小为8字节,10亿只实例需要80GB内存,超出硬件承载能力。
移除基类后,cat变为空类(大小1字节,10亿只仅需10GB内存),但失去了动态多态能力,无法复用make_noise、make_animal这类通用逻辑。手动实现Rust风格胖指针又会导致代码臃肿,且与标准库智能指针兼容性差。
需要一种方案:既让同类型数组避免虚表指针开销,又保留动态多态的灵活性,同时代码简洁易维护。
方案1:静态多态+轻量类型擦除分离处理
核心思路:编译期确定类型的场景(如大数组)用静态多态,运行期不确定类型的场景(如工厂函数)用轻量类型擦除,完全遵循C++“不使用则不付费”的设计原则。
第一步:实现静态多态的通用逻辑
保留无基类的动物类型,用模板实现静态多态的make_noise:
struct dog { std::string get_noise() const { return "Woof"; } }; struct cat { std::string get_noise() const { return "Meow"; } }; // 静态多态版本,编译期自动推导参数类型 template<typename Animal> void make_noise(const Animal& target) { std::cout << target.get_noise() << std::endl; }
此时std::vector<cat> cats(10000000000);的内存开销仅为10GB(空类大小1字节),完全可行,且make_noise(cats[0])可以直接调用。
第二步:实现轻量类型擦除包装类
用极简的类型擦除封装动态多态逻辑,避免手动维护虚表:
class animal { public: template<typename T> animal(T obj) : _get_noise([](const void* ptr) { return static_cast<const T*>(ptr)->get_noise(); }), _destroy([](void* ptr) { delete static_cast<T*>(ptr); }), _obj(new T(std::move(obj))) {} // 禁用拷贝,支持移动 animal(const animal&) = delete; animal& operator=(const animal&) = delete; animal(animal&& other) noexcept : _get_noise(std::exchange(other._get_noise, nullptr)), _destroy(std::exchange(other._destroy, nullptr)), _obj(std::exchange(other._obj, nullptr)) {} animal& operator=(animal&& other) noexcept { if (this != &other) { _destroy(_obj); _get_noise = std::exchange(other._get_noise, nullptr); _destroy = std::exchange(other._destroy, nullptr); _obj = std::exchange(other._obj, nullptr); } return *this; } ~animal() { if (_destroy && _obj) _destroy(_obj); } std::string get_noise() const { return _get_noise(_obj); } private: std::string (*_get_noise)(const void*); void (*_destroy)(void*); void* _obj; };
第三步:适配工厂函数
用标准库智能指针包装类型擦除类,兼容原有工厂逻辑:
std::unique_ptr<animal> make_animal(const std::string& type) { if (type == "dog") { return std::make_unique<animal>(dog{}); } else { return std::make_unique<animal>(cat{}); } }
该方案完美覆盖两种场景:
- 同类型大数组:直接使用原生类型,无虚表开销
- 动态类型场景:用
animal包装类实现多态,且与标准库智能指针完全兼容
方案2:用std::variant实现类型变体(限已知类型数量)
如果动物类型数量固定(仅dog、cat),可以用C++17引入的std::variant替代继承多态,结合std::visit实现通用操作:
#include <variant> struct dog { std::string get_noise() const { return "Woof"; } }; struct cat { std::string get_noise() const { return "Meow"; } }; // 定义动物类型变体 using animal_variant = std::variant<dog, cat>; // 通用make_noise,通过std::visit自动分发到对应类型 void make_noise(const animal_variant& animal) { std::visit([](const auto& obj) { std::cout << obj.get_noise() << std::endl; }, animal); } // 工厂函数返回变体类型 animal_variant make_animal(const std::string& type) { if (type == "dog") { return dog{}; } else { return cat{}; } }
优点:
- 同类型数组:
std::vector<cat>依然保持1字节大小,无额外开销 - 动态场景:
std::variant的大小为最大成员的大小(此处为1字节),比继承多态的虚表指针开销更小 - 类型安全:
std::visit强制覆盖所有变体类型,避免遗漏分支
方案3:CRTP静态多态+动态适配器(兼容旧多态接口)
如果需要兼容原有基于继承的多态接口,可以用CRTP实现静态多态,再封装动态适配器类:
// CRTP静态多态基类,复用通用逻辑 template<typename Derived> struct animal_static { std::string get_noise() const { return static_cast<const Derived*>(this)->get_noise(); } }; struct dog : animal_static<dog> { std::string get_noise() const { return "Woof"; } }; struct cat : animal_static<cat> { std::string get_noise() const { return "Meow"; } }; // 动态适配器类,用于运行期多态场景 class animal_dynamic { public: template<typename T> animal_dynamic(const T& obj) : _impl(std::make_unique<impl<T>>(obj)) {} std::string get_noise() const { return _impl->get_noise(); } private: struct base_impl { virtual ~base_impl() = default; virtual std::string get_noise() const = 0; }; template<typename T> struct impl : base_impl { impl(const T& obj) : _obj(obj) {} std::string get_noise() const override { return _obj.get_noise(); } T _obj; }; std::unique_ptr<base_impl> _impl; }; // 通用make_noise,同时支持静态和动态类型 template<typename Animal> void make_noise(const Animal& target) { std::cout << target.get_noise() << std::endl; }
优点:
- 静态场景:CRTP的空基类优化让
cat类保持1字节大小,数组无额外开销 - 动态场景:
animal_dynamic封装了多态细节,与标准库智能指针完全兼容 - 代码复用:静态多态基类可以复用通用逻辑,动态适配器仅处理运行期多态需求
所有方案的核心都是拆分场景按需处理:
- 编译期确定类型的场景:用静态多态或原生类型,彻底避免虚表指针的内存开销
- 运行期不确定类型的场景:用轻量类型擦除、
std::variant或动态适配器实现多态,避免手动维护虚表的繁琐
内容的提问来源于stack exchange,提问作者DutChen18

