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 可以确保在编译时进行类型检查。

定义派生类

定义两个派生类CircleRectangle

 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 的特有逻辑变多之后,这种设计就寄了。

参考