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