大家好,今天介绍 CRTP。最近我在捣鼓 Eigen 线代库,发现里面大量使用了这种模式,所以稍微研究一下。CRTP(Curiously Recurring Template Pattern)是 C++ 中的一种设计模式,特点是利用模板和继承,在基类关联派生类模板参数,来实现静态多态性。
为了更好理解,下面通过一个例子来解释 CRTP 的用法。
经典例子:形状的面积计算
假设要计算不同形状的面积,比如圆形和矩形。通过一个基类来实现公共的接口,同时每个形状能够提供自己的计算逻辑。
定义一个模板基类
Shape
,它接受一个派生类作为模板参数。
1#include <iostream>
2#include <cmath>
4template <typename Derived>
5class Shape {
6public:
7 void area() {
8 static_cast<Derived*>(this)->computeArea();
10};
基类中,
area
调用了派生类
computeArea
方法。用
static_cast
可以确保在编译时进行类型检查。
定义派生类
定义两个派生类
Circle
和
Rectangle
。
1class Circle : public Shape<Circle> {
2public:
3 Circle(double radius) : radius(radius) {}
5 void computeArea() {
6 double area = M_PI * radius * radius;
7 std::cout << "Circle area: " << area << std::endl;
10private:
11 double radius;
12};
14class Rectangle : public Shape<Rectangle> {
15public:
16 Rectangle(double width, double height) : width(width), height(height) {}
18 void computeArea() {
19 double area = width * height;
20 std::cout << "Rectangle area: " << area << std::endl;
23private:
24 double width, height;
25};
每个派生类实现
computeArea
方法。
使用 CRTP
创建实例,计算面积。
1int main() {
2 Circle circle(5.0);
3 circle.area(); // 输出:Circle area: 78.5398
5 Rectangle rectangle(4.0, 6.0);
6 rectangle.area(); // 输出:Rectangle area: 24
8 return 0;
对比虚函数多态的方式
在以往的典型虚函数方式下,我们是这么做的:
1#include <iostream>
2#include <cmath>
4class Shape {
5public:
6 virtual void computeArea() const = 0; // 纯虚函数
7 virtual ~Shape() = default; // 虚析构函数
10class Circle : public Shape {
11public:
12 Circle(double radius) : radius(radius) {}
14 void computeArea() const override {
15 double area = M_PI * radius * radius;
16 std::cout << "Circle area: " << area << std::endl;
19private:
20 double radius;
21};
23class Rectangle : public Shape {
24public:
25 Rectangle(double width, double height) : width(width), height(height) {}
27 void computeArea() const override {
28 double area = width *
height;
29 std::cout << "Rectangle area: " << area << std::endl;
32private:
33 double width, height;
34};
36int main() {
37 Shape* circle = new Circle(5.0);
38 circle->computeArea(); // 输出:Circle area: 78.5398
40 Shape* rectangle = new Rectangle(4.0, 6.0);
41 rectangle->computeArea(); // 输出:Rectangle area: 24
43 delete circle;
44 delete rectangle;
46 return 0;
主要区别:
16
class
Rectangle
{
17
public
:
18
Rectangle(
double
l,
double
w)
:
length(l), width(w) {}
20
double
getArea
()
const
{
21
return
length
*
width;
24
private
:
25
double
length;
26
double
width;
27
};
29
template
<
typename
Shape
>
30
double
calculateArea(
const
Shape
&
shape) {
31
return
shape.getArea();
34
int
main
() {
35
Circle circle(
5.0
);
36
Rectangle rectangle(
10.0
,
4.0
);
38
std
::
cout
<<
"Circle area: "
<<
calculateArea(circle)
<<
std
::
endl;
39
std
::
cout
<<
"Rectangle area: "
<<
calculateArea(rectangle)
<<
std
::
endl;
41
return
0
;
个人认为,这种方式的话可读性更好一点,性能和 CRTP 是一个级别的。不过,由于没有明确的协议(例如以虚函数或者父类成员形式提供的接口),需要小心确保所有类型具有相同的接口,而且不太能很好地利用编译器的补全。
对比 Rust 的 Enum 静态多态
好吧,虽然是 C++ 专题,但都是一个时代的语言(
1use std::f64::consts::PI;
3enum Shape {
4 Circle(f64),
5 Rectangle(f64, f64),
8impl Shape {
9 fn area(&self) -> f64 {
10 match self {
11 Shape::Circle(radius) => PI * radius * radius,
12 Shape::Rectangle(length, width) => length * width,
13 }
14 }
17fn main() {
18 let circle = Shape::Circle(5.0);
19 let rectangle = Shape::Rectangle(10.0, 4.0);
21 println!("Circle area: {}", circle.area());
22 println!("Rectangle area: {}", rectangle.area());
个人认为,Rust 这种方式的直观性应该是最好的。可以在一个类型中定义多个变体,方便管理不同形状。不足之处在于每次扩展都需要分散修改多处代码,当存在需要协作开发的多模块的依赖的时候就比较难受了。
头脑风暴:C++ 能不能也搞个枚举多态?
C++中,虽然没有直接与Rust的枚举完全等价的机制,但可以通过使用联合和结构体来实现类似的静态多态。
1#include <iostream>
2#include <cmath>
4enum class ShapeType {
5 Circle,
6 Rectangle
9union ShapeData {
10 double radius; // 用于Circle
11 struct {
12 double length;
13 double width;
14 } rectangle; // 用于Rectangle
16 ShapeData() {} // 默认构造函数
17 ~ShapeData() {} // 析构函数
18};
20struct Shape {
21 ShapeType type;
22 ShapeData data;
24 Shape(double radius) {
25 type = ShapeType::Circle;
26 data.radius = radius;
29 Shape(double length, double width) {
30 type = ShapeType::Rectangle;
31 data.rectangle.length = length;
32 data.rectangle.width = width;
35 double area() const {
36 switch (type) {
37 case ShapeType::Circle:
38 return M_PI * data.radius * data.radius;
39 case ShapeType::Rectangle:
40 return data.rectangle.length * data.rectangle.width;
41 default:
42 throw std::runtime_error("Unknown shape type");
45};
47int main() {
48 Shape circle(5.0);
49 Shape rectangle(10.0, 4.0);
51 std::cout << "Circle area: " << circle.area() << std::endl;
52 std::cout << "Rectangle area: " << rectangle.area() << std::endl;
53 return 0;
这样性能应该也不差。不过,ShapeData 这个设计实在太丑陋了。(Rust 底层估计也这么丑,但是开发者看到的部分还能接受)当各个 Shape 的特有逻辑变多之后,这种设计就寄了。
Curiously recurring template pattern - Wikipedia