一文讲清 C++ CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)
CRTP是 C++ 中的一种模板元编程技术,其核心原理是通过模板继承和静态多态,在编译期实现基类对派生类成员的访问,从而避免运行时虚函数调用的开销。
1. CRTP 的基本结构
CRTP 的核心思想是:基类是一个模板类,其模板参数是派生类本身。
典型的代码如下:
template <typename Derived>
class Base {
public:
void interface() {
// 基类通过静态转换调用派生类的实现
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> { // 派生类将自己作为模板参数传递给基类
public:
void implementation() {
// 派生类的具体实现
}
};
2. CRTP 的核心原理
2.1 模板继承
2.1.1基类模板化:基类 Base
接受派生类 Derived
作为模板参数
2.1.2派生类继承基类模板:派生类继承时,将自己作为模板参数传递给基类,形成递归依赖(Derived : public Base<Derived>
)
2.2 静态多态
2.2.1 编译期绑定:基类通过 static_cast<Derived*>(this)
将 this
指针转换为派生类类型,直接调用派生类的方法(如 implementation()
)。
2.2.2 无虚函数开销:无需虚函数表(vtable),函数调用在编译期确定,无运行时间接跳转。
2.2.3 代码复用与扩展:基类可定义通用逻辑(如 interface()
),而具体实现由派生类提供(如 implementation()
)。新增派生类时,只需实现特定方法,无需修改基类。
3 CRTP 的工作流程
3.1模板实例化:当定义 Derived
类时,基类 Base<Derived>
被实例化,生成针对 Derived
的基类代码
3.2方法调用:调用 Base<Derived>::interface()
时,通过 static_cast
将基类指针安全转换为派生类指针,直接调用 Derived::implementation()
3.3编译期解析:所有类型转换和函数绑定在编译期完成,生成高效代码
4 CRTP示例
#include <iostream>
using namespace std;
// 定义一个模板基类,接受派生类作为参数
template<typename T>
class Animal {
public:
void sound() {
// static_cast到派生类类型,调用派生类的soundImpl()
cout << "Animal::sound" <<endl;
static_cast<T*>(this)->soundImpl();
}
};
// 具体的狗类,继承自Animal<Dog>
class Dog : public Animal<Dog> {
public:
// 具体实现狗的叫声
void soundImpl() {
cout << "Woof!" << endl;
}
};
// 具体的猫类,继承自Animal<Cat>
class Cat : public Animal<Cat> {
public:
void soundImpl() {
cout << "Meow!" << endl;
}
};
int main() {
// 创建一个Dog对象,调用sound()
Animal<Dog> dog;
dog.sound(); // 输出 Woof!
// 创建一个Cat对象,调用sound()
Animal<Cat> cat;
cat.sound(); // 输出 Meow!
return 0;
}
//运行结果:
//Animal::sound
//Woof!
//Animal::sound
//Meow!
4.1汇编分析:
.LC0:
.string "Woof!"
Dog::soundImpl():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char>>& std::endl<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&)
mov rdi, rax
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
nop
leave
ret
.LC1:
.string "Meow!"
Cat::soundImpl():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char>>& std::endl<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&)
mov rdi, rax
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
nop
leave
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-1]
mov rdi, rax
call Animal<Dog>::sound()
lea rax, [rbp-2]
mov rdi, rax
call Animal<Cat>::sound()
mov eax, 0
leave
ret
.LC2:
.string "Animal::sound"
Animal<Dog>::sound():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC2
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char>>& std::endl<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&)
mov rdi, rax
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Dog::soundImpl()
nop
leave
ret
Animal<Cat>::sound():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC2
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char>>& std::endl<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&)
mov rdi, rax
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Cat::soundImpl()
nop
leave
ret
重点关注一下main函数中对sound的调用
lea rax, [rbp-1] ; 对象地址
mov rdi, rax
call Dog::sound() ; 直接调用
5 同样的功能,用动态多态实现
class Animal
{
public:
virtual void sound()
{
cout << "Animal::sound" <<endl;
}
};
class Dog : public Animal
{
virtual void sound()
{
cout << "Woof!" << endl;
}
};
class Cat: public Animal
{
virtual void sound()
{
cout << "Meow!" << endl;
}
};
int main() {
Animal* animal1 = new Dog();
animal1->sound();
Animal* animal2 = new Cat();
animal2->sound();
return 0;
}
//程序运行结果:
//Woof!
//Meow!
对应的汇编:
.LC0:
.string "Animal::sound"
Animal::sound():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char>>& std::endl<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&)
mov rdi, rax
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
nop
leave
ret
.LC1:
.string "Woof!"
Dog::sound():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char>>& std::endl<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&)
mov rdi, rax
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
nop
leave
ret
.LC2:
.string "Meow!"
Cat::sound():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov esi, OFFSET FLAT:.LC2
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&, char const*)
mov esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char>>& std::endl<char, std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&)
mov rdi, rax
call std::ostream::operator<<(std::ostream& (*)(std::ostream&))
nop
leave
ret
Animal::Animal() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov edx, OFFSET FLAT:vtable for Animal+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
pop rbp
ret
Dog::Dog() [base object constructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Animal::Animal() [base object constructor]
mov edx, OFFSET FLAT:vtable for Dog+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
leave
ret
Cat::Cat() [base object constructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Animal::Animal() [base object constructor]
mov edx, OFFSET FLAT:vtable for Cat+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
leave
ret
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
mov edi, 8
call operator new(unsigned long)
mov rbx, rax
mov QWORD PTR [rbx], 0
mov rdi, rbx
call Dog::Dog() [complete object constructor]
mov QWORD PTR [rbp-24], rbx
mov rax, QWORD PTR [rbp-24]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-24]
mov rdi, rax
call rdx
mov edi, 8
call operator new(unsigned long)
mov rbx, rax
mov QWORD PTR [rbx], 0
mov rdi, rbx
call Cat::Cat() [complete object constructor]
mov QWORD PTR [rbp-32], rbx
mov rax, QWORD PTR [rbp-32]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-32]
mov rdi, rax
call rdx
mov eax, 0
mov rbx, QWORD PTR [rbp-8]
leave
ret
vtable for Cat:
.quad 0
.quad typeinfo for Cat
.quad Cat::sound()
vtable for Dog:
.quad 0
.quad typeinfo for Dog
.quad Dog::sound()
vtable for Animal:
.quad 0
.quad typeinfo for Animal
.quad Animal::sound()
typeinfo for Cat:
.quad vtable for __cxxabiv1::__si_class_type_info+16
.quad typeinfo name for Cat
.quad typeinfo for Animal
typeinfo name for Cat:
.string "3Cat"
typeinfo for Dog:
.quad vtable for __cxxabiv1::__si_class_type_info+16
.quad typeinfo name for Dog
.quad typeinfo for Animal
typeinfo name for Dog:
.string "3Dog"
typeinfo for Animal:
.quad vtable for __cxxabiv1::__class_type_info+16
.quad typeinfo name for Animal
typeinfo name for Animal:
.string "6Animal"
重点关注一下main函数中sound的调用过程
push rbp ; 保存旧的基址指针(rbp)
mov rbp, rsp ; 将当前栈指针(rsp)设为新的基址指针(rbp)
push rbx ; 保存rbx寄存器(被调用者保存寄存器)
sub rsp, 24 ; 分配24字节栈空间(用于局部变量和对齐)
mov edi, 8 ; 参数:申请8字节内存(Dog对象的大小)
call operator new(unsigned long) ; 调用new运算符分配内存
mov rbx, rax ; 将返回的指针保存到rbx(rax存放new的返回值)
mov QWORD PTR [rbx], 0 ; 将内存的前8字节置零(临时初始化虚表指针vptr)
mov rdi, rbx ; 将对象指针作为构造函数参数(this指针)
call Dog::Dog() [complete object constructor] ; 调用Dog构造函数
mov QWORD PTR [rbp-24], rbx ; 将构造后的Dog指针存入栈[rbp-24]
//对应C++代码:
//Animal* dog = new Dog(); // 动态分配Dog对象
//调用Dog对象的虚函数
mov rax, QWORD PTR [rbp-24] ; 从栈中加载Dog对象指针到rax
mov rax, QWORD PTR [rax] ; 加载虚表指针vptr(Dog的虚表地址)
mov rdx, QWORD PTR [rax] ; 加载虚表第一个条目(即Dog::sound()地址)
mov rax, QWORD PTR [rbp-24] ; 再次加载Dog对象指针到rax
mov rdi, rax ; 将对象指针作为this参数传递
call rdx ; 通过虚表调用Dog::sound()
//对应C++代码:
//dog->sound(); // 动态绑定到Dog::sound()
6 通过对比,关键差异总结如下:
特性 | 动态多态(虚函数) | 静态多态(CRTP) |
---|---|---|
实现方式 | 虚函数表 + 运行时间接调用 | 模板继承 + 编译期绑定 |
内存开销 | 每个对象需存储 vptr (通常8字节) | 无额外内存开销 |
性能 | 函数调用需查表跳转(轻微性能损耗) | 直接调用(无额外开销) |
灵活性 | 支持运行时多态 | 仅支持编译期多态 |
代码扩展性 | 新增派生类无需修改基类 | 需修改模板参数或继承关系 |
汇编特征 | 虚表结构、vptr 初始化、间接调用(call rdx ) | 直接函数调用(call Dog::sound() ) |