C++ CRTP
CRTP(curiously recurring template pattern, 奇异递归模式),这个名字奇怪的模式,是一种将继承和静态多态结合的技术。
多态是一种用单个统一的符号将多种特定行为关联起来的能力,是面向对象编的基石,在 C++中它主要由继承和虚函数实现。由于这一机制主要(至少是一部分)在运行期间起作用,因此我们称之为动态多态(dynamic polymorphism)。它也是我们通常在讨论 C++中的简单多态时所指的多态。但是,模板也允许我们用单个统一符号将不同的特定行为 关联起来,不过该关联主要发生在编译期间,我们称之为静态多态(static polymorphism)。
1. 动态多态实现
假设有一个基类Base
,其中有一个纯虚函数impl()
1
2
3
4
5
class Base
{
public:
virtual void impl()=0;
};
接着有两个Base
的派生类:D1
,D2
,各自实现了impl()
函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class D1:public Base
{
public:
virtual void impl() override
{
std::cout<<"D1:impl()"<<std::endl;
}
};
class D2:public Base
{
public:
virtual void impl() override
{
std::cout<<"D2:impl()"<<std::endl;
}
};
我们可以在函数中调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
void exec(Base &obj)
{
obj.impl();
}
int main()
{
D1 d1;
D2 d2;
exec(d1);
exec(d2);
return 0;
}
结果就会打印输出:
1
2
D1::impl()
D2::impl()
2. 静态多态实现
同样可以用静态多态来实现上面的功能,静态多态通过模板来实现,首先同样是定义类D1
和D2
,不同的是这回它们不再是派生类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class D1
{
public:
void impl()
{
std::cout<<"D1:impl()"<<std::endl;
}
};
class D2
{
public:
void impl()
{
std::cout<<"D2:impl()"<<std::endl;
}
};
接着实现一个模板函数:
1
2
3
4
5
template <typename T>
void exec(T obj)
{
obj.impl();
}
我们就可以调用这个函数达到多态的目的:
1
2
3
4
5
6
7
8
int main()
{
D1 d1;
D2 d2;
exec(d1);
exec(d2);
return 0;
}
结果同样会打印输出:
1
2
D1::impl()
D2::impl()
静态多态在编译期就确认了exec
中具体调用哪个函数,比动态多态在运行期根据虚函数表去查找有性能的提升。但是从静态多态的例子中我们可以发现,D1
和D2
不是继承某个基类,意味着没有代码复用。就算D1
类和D2
类中其他函数都相同,只有impl
函数的实现不同,D1
和D2
类还是要重复那些实现相同的函数。
3. CRTP模式实现
CRTP模式将继承和静态多态结合,既能通过静态多态提升性能,也能通过继承进行代码复用。
CRTP实际实现是将派生类作为模板参数传递给其某个基类。
首先定义一个模板基类Base
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
class Base
{
void impl()
{
static_cast<T *>(this)->impl();
}
void exec()
{
std::cout<<"Base::exec()"<<std::endl;
}
}
接着D1
和D2
分别将自己作为模板参数传递给Base
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class D1:Base<D1>
{
public:
void impl()
{
std::cout<<"D1:impl()"<<std::endl;
}
};
class D2:Base<D2>
{
public:
void impl()
{
std::cout<<"D2:impl()"<<std::endl;
}
};
我们就可以使用:
1
2
3
4
5
6
7
8
9
10
int main()
{
D1 d1;
D2 d2;
d1.impl();
d2.impl();
d1.exec();
d2.exec();
return 0;
}
输出结果是:
1
2
3
4
D1::impl()
D2::impl()
Base::exec()
Base::exec()
CRTP的例子中我们可以发现,D1
和D2
缺少共同的基类,没错,D1
和D2
继承的不是同一个基类。 D1
的基类是Base<D1>
,D2
的基类是Base<D2>
。
因此,每当需要一个共同的基类时,例如,为了在一个集合中存储不同类型而需要的共同抽象,CRTP设计模式就不是正确的选择。
4. 使用CRTP的环境
性能在C++中极为重要,而使用虚函数存在性能开销。因此,在对性能敏感的环境中,例如计算机游戏或高频交易的某些部分,不会使用虚函数。在高性能计算(HPC)中也是如此。在HPC中,任何类型的条件判断或间接寻址,包括虚函数,都被禁止在性能最关键的部分使用。