头图

大家好,今天介绍 CRTP。最近我在捣鼓 Eigen 线代库,发现里面大量使用了这种模式,所以稍微研究一下。CRTP(Curiously Recurring Template Pattern)是 C++ 中的一种设计模式,特点是利用模板和继承,在基类关联派生类模板参数,来实现静态多态性。

为了更好理解,下面通过一个例子来解释 CRTP 的用法。

经典例子:形状的面积计算

假设要计算不同形状的面积,比如圆形和矩形。通过一个基类来实现公共的接口,同时每个形状能够提供自己的计算逻辑。

定义基类

定义一个模板基类 Shape,它接受一个派生类作为模板参数。

#include <iostream>
#include <cmath>

template <typename Derived>
class Shape {
public:
    void area() {
        static_cast<Derived*>(this)->computeArea();
    }
};

基类中,area 调用了派生类 computeArea 方法。用 static_cast 可以确保在编译时进行类型检查。

定义派生类

定义两个派生类CircleRectangle

class Circle : public Shape<Circle> {
public:
    Circle(double radius) : radius(radius) {}

    void computeArea() {
        double area = M_PI * radius * radius;
        std::cout << "Circle area: " << area << std::endl;
    }

private:
    double radius;
};

class Rectangle : public Shape<Rectangle> {
public:
    Rectangle(double width, double height) : width(width), height(height) {}

    void computeArea() {
        double area = width * height;
        std::cout << "Rectangle area: " << area << std::endl;
    }

private:
    double width, height;
};

每个派生类实现 computeArea 方法。

使用 CRTP

创建实例,计算面积。

int main() {
    Circle circle(5.0);
    circle.area();  // 输出:Circle area: 78.5398

    Rectangle rectangle(4.0, 6.0);
    rectangle.area();  // 输出:Rectangle area: 24

    return 0;
}

对比虚函数多态的方式

在以往的典型虚函数方式下,我们是这么做的:

#include <iostream>
#include <cmath>

class Shape {
public:
    virtual void computeArea() const = 0;  // 纯虚函数
    virtual ~Shape() = default;  // 虚析构函数
};

class Circle : public Shape {
public:
    Circle(double radius) : radius(radius) {}

    void computeArea() const override {
        double area = M_PI * radius * radius;
        std::cout << "Circle area: " << area << std::endl;
    }

private:
    double radius;
};

class Rectangle : public Shape {
public:
    Rectangle(double width, double height) : width(width), height(height) {}

    void computeArea() const override {
        double area = width * height;
        std::cout << "Rectangle area: " << area << std::endl;
    }

private:
    double width, height;
};

int main() {
    Shape* circle = new Circle(5.0);
    circle->computeArea();  // 输出:Circle area: 78.5398

    Shape* rectangle = new Rectangle(4.0, 6.0);
    rectangle->computeArea();  // 输出:Rectangle area: 24

    delete circle;
    delete rectangle;

    return 0;
}

主要区别:

特性虚函数CRTP
多态性运行时多态静态多态
性能开销有虚表开销无虚函数表开销
类型安全在运行时检查在编译时检查
代码维护容易可能更复杂,编译时间较长
使用场景需要灵活性(比如实现在另一个对象文件)和动态性性能敏感和静态多态的场景

对比模板(无继承)的方式

同样是静态多态,还可以这样实现:

#include <iostream>
#include <cmath>

class Circle {
public:
    Circle(double r) : radius(r) {}

    double getArea() const {
        return M_PI * radius * radius;
    }

private:
    double radius;
};

class Rectangle {
public:
    Rectangle(double l, double w) : length(l), width(w) {}

    double getArea() const {
        return length * width;
    }

private:
    double length;
    double width;
};

template <typename Shape>
double calculateArea(const Shape& shape) {
    return shape.getArea();
}

int main() {
    Circle circle(5.0);
    Rectangle rectangle(10.0, 4.0);

    std::cout << "Circle area: " << calculateArea(circle) << std::endl;
    std::cout << "Rectangle area: " << calculateArea(rectangle) << std::endl;

    return 0;
}

个人认为,这种方式的话可读性更好一点,性能和 CRTP 是一个级别的。不过,由于没有明确的协议(例如以虚函数或者父类成员形式提供的接口),需要小心确保所有类型具有相同的接口,而且不太能很好地利用编译器的补全。

对比 Rust 的 Enum 静态多态

好吧,虽然是 C++ 专题,但都是一个时代的语言(

use std::f64::consts::PI;

enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(radius) => PI * radius * radius,
            Shape::Rectangle(length, width) => length * width,
        }
    }
}

fn main() {
    let circle = Shape::Circle(5.0);
    let rectangle = Shape::Rectangle(10.0, 4.0);

    println!("Circle area: {}", circle.area());
    println!("Rectangle area: {}", rectangle.area());
}

个人认为,Rust 这种方式的直观性应该是最好的。可以在一个类型中定义多个变体,方便管理不同形状。不足之处在于每次扩展都需要分散修改多处代码,当存在需要协作开发的多模块的依赖的时候就比较难受了。

头脑风暴:C++ 能不能也搞个枚举多态?

C++中,虽然没有直接与Rust的枚举完全等价的机制,但可以通过使用联合和结构体来实现类似的静态多态。

#include <iostream>
#include <cmath>

enum class ShapeType {
    Circle,
    Rectangle
};

union ShapeData {
    double radius; // 用于Circle
    struct {
        double length;
        double width;
    } rectangle; // 用于Rectangle

    ShapeData() {} // 默认构造函数
    ~ShapeData() {} // 析构函数
};

struct Shape {
    ShapeType type;
    ShapeData data;

    Shape(double radius) {
        type = ShapeType::Circle;
        data.radius = radius;
    }

    Shape(double length, double width) {
        type = ShapeType::Rectangle;
        data.rectangle.length = length;
        data.rectangle.width = width;
    }

    double area() const {
        switch (type) {
            case ShapeType::Circle:
                return M_PI * data.radius * data.radius;
            case ShapeType::Rectangle:
                return data.rectangle.length * data.rectangle.width;
            default:
                throw std::runtime_error("Unknown shape type");
        }
    }
};

int main() {
    Shape circle(5.0);
    Shape rectangle(10.0, 4.0);

    std::cout << "Circle area: " << circle.area() << std::endl;
    std::cout << "Rectangle area: " << rectangle.area() << std::endl;
    return 0;
}

这样性能应该也不差。不过,ShapeData 这个设计实在太丑陋了。(Rust 底层估计也这么丑,但是开发者看到的部分还能接受)当各个 Shape 的特有逻辑变多之后,这种设计就寄了。

参考

我的博客:

C++:使用 CRTP 模式实现静态多态
https://www.less-bug.com/posts/cpp-static-polymorphism-using-...

Github:
https://github.com/pluveto


pluvet
115 声望2 粉丝

东方众,口琴党