如何利用C++23的推导this结合CRTP管理集合中的多态行为
嘿,这个需求我之前也折腾过!C++23的推导this(就是带this auto&&的成员函数)确实给CRTP静态多态注入了新活力,既能保留静态绑定的性能优势,又能优雅地管理不同派生类的集合,咱们一步步来拆解怎么实现。
先搞懂推导this给CRTP带来的核心便利
以前写CRTP,基类里总得写个static_cast<Derived&>(*this)来转成派生类实例,不仅代码啰嗦,还容易写错模板参数。现在有了推导this,基类的成员函数可以直接用this auto&& self作为参数,编译器会自动把self推导成当前调用对象的实际类型——也就是你的派生类实例,完全不用手动转换,代码清爽多了,甚至还能把CRTP基类做成非模板类,进一步降低复杂度。
举个最基础的CRTP+推导this的例子:
// 非模板的CRTP基类!靠推导this自动适配所有派生类 struct ShapeCRTP { // 用this auto&& self获取派生类实例 void draw(this auto&& self) const { // 直接调用派生类的具体实现 self.draw_impl(); } double calculate_area(this auto&& self) const { return self.area_impl(); } }; // 派生类:圆形 struct Circle : ShapeCRTP { double radius; explicit Circle(double r) : radius(r) {} // 只需要实现具体业务逻辑 void draw_impl() const { std::cout << "画一个半径为" << radius << "的圆\n"; } double area_impl() const { return 3.14159 * radius * radius; } }; // 派生类:正方形 struct Square : ShapeCRTP { double side; explicit Square(double s) : side(s) {} void draw_impl() const { std::cout << "画一个边长为" << side << "的正方形\n"; } double area_impl() const { return side * side; } };
看,派生类只需要专注实现业务逻辑draw_impl和area_impl,CRTP基类的通用方法自动转发,完全不用管类型转换那堆事,比传统CRTP省心太多。
方案1:用std::variant做静态分发的集合(无动态分配、无虚函数开销)
如果你的场景里所有派生类类型都是已知的,那用std::variant来存不同的派生类是最优解——没有动态内存分配,没有虚函数调用开销,完全是编译期静态绑定,性能拉满。
直接上完整代码:
#include <vector> #include <variant> #include <iostream> #include <utility> struct ShapeCRTP { void draw(this auto&& self) const { self.draw_impl(); } double calculate_area(this auto&& self) const { return self.area_impl(); } }; struct Circle : ShapeCRTP { double radius; explicit Circle(double r) : radius(r) {} void draw_impl() const { std::cout << "圆形(r=" << radius << ")\n"; } double area_impl() const { return 3.14159 * radius * radius; } }; struct Square : ShapeCRTP { double side; explicit Square(double s) : side(s) {} void draw_impl() const { std::cout << "正方形(s=" << side << ")\n"; } double area_impl() const { return side * side; } }; // 定义variant类型,包含所有可能的形状 using Shape = std::variant<Circle, Square>; // 通用的访问工具函数,用std::visit做编译期分发 template<typename Func> void process_shape(const Shape& shape, Func&& func) { std::visit(std::forward<Func>(func), shape); } int main() { // 用vector存Shape(其实是variant) std::vector<Shape> shapes; shapes.emplace_back(Circle(2.0)); shapes.emplace_back(Square(3.0)); // 遍历处理每个形状 for (const auto& shape : shapes) { process_shape(shape, [](const auto& s) { s.draw(); std::cout << "面积:" << s.calculate_area() << "\n"; }); } }
关键说明
std::variant是类型安全的联合体,所有派生类都存在栈上(如果对象不大的话),没有堆分配开销。std::visit会在编译期生成所有可能的分支,完全是静态绑定,性能和直接调用派生类方法一样。- 推导this在这里的作用就是让CRTP基类的方法能无缝适配variant里的任意派生类,不用写一堆重载。
方案2:用类型擦除做灵活分发(支持未知派生类)
如果你的场景里无法提前知道所有派生类类型,那可以用类型擦除来做一个通用的包装类,兼顾灵活性和性能——虽然有一次虚函数调用和动态分配,但比传统的虚函数继承更灵活,因为派生类只需要实现CRTP要求的impl方法,不用直接继承虚基类。
完整代码示例:
#include <vector> #include <memory> #include <iostream> #include <utility> #include <type_traits> // 非模板CRTP基类 struct ShapeCRTP { void draw(this auto&& self) const { self.draw_impl(); } double calculate_area(this auto&& self) const { return self.area_impl(); } }; // 类型擦除的包装类,作为集合的公共接口 class Shape { // 内部的概念类,定义多态接口 struct Concept { virtual ~Concept() = default; virtual void draw() const = 0; virtual double calculate_area() const = 0; }; // 模型类,包装具体的派生类 template<typename T> struct Model final : Concept { T value; template<typename U> explicit Model(U&& v) : value(std::forward<U>(v)) {} void draw() const override { // 调用CRTP基类的draw方法,自动转发到派生类的impl value.draw(); } double calculate_area() const override { return value.calculate_area(); } }; std::unique_ptr<Concept> pimpl_; public: // 构造函数,接受任意CRTP派生类 template<typename T, std::derived_from<ShapeCRTP> D = std::decay_t<T>> explicit Shape(T&& t) : pimpl_(std::make_unique<Model<D>>(std::forward<T>(t))) {} // 转发到pimpl的方法 void draw() const { pimpl_->draw(); } double calculate_area() const { return pimpl_->calculate_area(); } }; // 派生类1:圆形 struct Circle : ShapeCRTP { double radius; explicit Circle(double r) : radius(r) {} void draw_impl() const { std::cout << "圆形(r=" << radius << ")\n"; } double area_impl() const { return 3.14159 * radius * radius; } }; // 派生类2:正方形 struct Square : ShapeCRTP { double side; explicit Square(double s) : side(s) {} void draw_impl() const { std::cout << "正方形(s=" << side << ")\n"; } double area_impl() const { return side * side; } }; // 可以随时加新的派生类,不用修改Shape类 struct Triangle : ShapeCRTP { double base, height; Triangle(double b, double h) : base(b), height(h) {} void draw_impl() const { std::cout << "三角形(b=" << base << ", h=" << height << ")\n"; } double area_impl() const { return 0.5 * base * height; } }; int main() { std::vector<Shape> shapes; shapes.emplace_back(Circle(2.0)); shapes.emplace_back(Square(3.0)); shapes.emplace_back(Triangle(4.0, 5.0)); for (const auto& shape : shapes) { shape.draw(); std::cout << "面积:" << shape.calculate_area() << "\n"; } }
关键说明
Shape类是类型擦除的包装,对外提供统一的接口,内部用pimpl指向具体的Model实例。- 新的派生类只需要继承
ShapeCRTP并实现impl方法,不用修改Shape类,完全符合开闭原则。 - 推导this让CRTP基类的实现极其简洁,没有任何类型转换的冗余代码。
总结一下两种方案的适用场景
- 用variant的静态分发:适合提前知道所有派生类的场景,性能最优,无动态分配,完全静态绑定。
- 用类型擦除的灵活分发:适合无法提前枚举所有派生类的场景,灵活性高,性能略逊于静态分发,但比传统虚函数继承更易用。
不管哪种方案,C++23的推导this都让CRTP的实现变得更简洁、更安全,再也不用写那些容易出错的static_cast了。
备注:内容来源于stack exchange,提问作者Roger World

