大家好,今天介绍 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
可以确保在编译时进行类型检查。
定义派生类
定义两个派生类Circle
和 Rectangle
。
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。