添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

大家好,今天介绍 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
  •