关于CRTP实现接口的C++类实例sizeof计算的疑问
我有一个关于使用CRTP实现接口的类实例sizeof计算的问题,示例代码如下:
#include <iostream> class Interface { public: virtual int foo() { std::cout << "Interface\n"; return 0; }; }; template <class Derived> class Base : Interface { public: virtual int foo() override { Interface::foo(); std::cout << "base\n"; return static_cast<Derived*>(this)->bar2; } int bar; }; class Derived : public Base<Derived> { public: virtual int foo() override { std::cout << "Derived\n"; Base<Derived>::foo(); return bar; } int bar2; }; int main() { std::cout << sizeof(Base<Derived>) << '\n'; std::cout << sizeof(Derived) << '\n'; return 0; }
经gcc和clang编译后,sizeof(Base<Derived>)与sizeof(Derived)的输出均为16,将Base逻辑全部移至Derived类中结果也一致。我原本预期Derived实例需存储3个虚表指针与2个int成员,总大小应为40字节,因此产生以下疑问:
- 为何会出现该结果?
- 编译器是否因CRTP合并了
Base<Derived>与Derived类? - 如何避免额外虚表指针开销?
- 该实现方式是否存在隐藏开销?
问题解答
1. 为何结果是16字节?
C++中,单一继承链只会维护一个虚表指针,不管继承层级有多少层。你的类继承链是Derived -> Base<Derived> -> Interface,这是一条单一的继承路径,所以整个对象只会包含一个虚表指针(64位系统下占8字节),加上两个int成员(每个4字节,共8字节),总大小就是8+4+4=16字节,和你看到的结果一致。
你误以为会有3个虚表指针,是错误理解了虚表指针的存储规则:只有当类存在多继承(多个独立的基类都有虚函数)时,才会为每个带虚函数的基类存储一个虚表指针。单一继承链里,所有虚函数共享同一个虚表指针,子类会覆盖父类的虚表条目,不会新增虚表指针。
2. 编译器是否因CRTP合并了Base<Derived>与Derived类?
没有合并类,CRTP本身只是一种模板编程技巧,编译器不会因为CRTP就合并类的结构。这里的大小计算符合普通单一继承的规则,和CRTP无关——哪怕把Base改成非模板类,只要是单一继承,结果还是一样的。
3. 如何避免额外虚表指针开销?
如果你的场景不需要运行时多态(也就是不需要通过基类指针调用子类的虚函数),可以去掉所有virtual关键字,利用CRTP的静态多态特性来实现接口:
- 把
Interface改成纯模板的静态接口,去掉虚函数; Base<Derived>中直接调用Derived的成员函数,不需要virtual;- Derived类不需要重写
virtual函数,而是实现约定好的接口函数。
示例修改后的代码:
#include <iostream> template <class Derived> class Base { public: int foo() { std::cout << "base\n"; return static_cast<Derived*>(this)->bar2; } int bar; }; class Derived : public Base<Derived> { public: int foo() { std::cout << "Derived\n"; Base<Derived>::foo(); return this->bar; } int bar2; }; int main() { std::cout << sizeof(Base<Derived>) << '\n'; // 输出8(64位系统下,一个int) std::cout << sizeof(Derived) << '\n'; // 输出16(两个int) }
这样修改后,类里没有虚表指针,完全通过编译期静态绑定实现接口,彻底消除虚表指针的开销。
如果必须保留运行时多态,那单一继承的结构已经是最优的,只会有一个虚表指针,无法再减少——这是运行时多态的必要开销。
4. 该实现方式是否存在隐藏开销?
在你当前的代码中:
- 没有额外的隐藏开销,单一继承的虚表指针是运行时多态的必要成本,两个int成员是显式的存储开销;
- CRTP本身不会带来额外开销,因为模板实例化是编译期完成的,不会产生运行时的额外成本;
- 需要注意的是,如果你错误地使用多继承(比如让Derived同时继承多个带虚函数的基类),才会新增虚表指针的开销,你的当前代码是单一继承,不存在这个问题。
内容的提问来源于stack exchange,提问作者Ahmed AEK

