如何用C++20 Concepts替代抽象接口作为shared_ptr模板参数及构建容器
用C++20 Concepts实现可Hittable对象容器的方案
Concept是编译期约束,不能直接作为指针类型存储(这就是你遇到“不完整类型”错误的原因)。要在保留Concept约束的同时最小化运行时开销,有两种可行方案:
方案一:自定义轻量类型擦除(与原虚函数开销一致)
这种方案不需要让所有Hittable类型继承统一基类,而是通过包装类自动适配符合Concept的类型,保持虚函数级别的运行时开销。
步骤1:定义Hittable Concept
#include <concepts> #include <memory> #include <vector> // 假设已有Ray和HitRecord定义 struct Ray {}; struct HitRecord { double t; /* 其他字段 */ }; template<typename T> concept Hittable = requires(const T& a, const Ray& r, double t_min, double t_max, HitRecord& rec) { { a.hit(r, t_min, t_max, rec) } -> std::convertible_to<bool>; };
步骤2:实现类型擦除包装类
// 底层抽象类(仅用于类型擦除,无需业务类继承) class HittableErased { public: virtual ~HittableErased() = default; virtual bool hit(const Ray& r, double t_min, double t_max, HitRecord& rec) const = 0; }; // 模板实现类,自动适配所有符合Hittable Concept的类型 template<Hittable T> class HittableErasedImpl : public HittableErased { public: explicit HittableErasedImpl(std::shared_ptr<T> obj) : obj_(std::move(obj)) {} bool hit(const Ray& r, double t_min, double t_max, HitRecord& rec) const override { return obj_->hit(r, t_min, t_max, rec); } private: std::shared_ptr<T> obj_; }; // 对外暴露的包装类 class HittableWrapper { public: template<Hittable T> explicit HittableWrapper(std::shared_ptr<T> obj) : impl_(std::make_unique<HittableErasedImpl<T>>(std::move(obj))) {} bool hit(const Ray& r, double t_min, double t_max, HitRecord& rec) const { return impl_->hit(r, t_min, t_max, rec); } private: std::unique_ptr<HittableErased> impl_; };
步骤3:实现HittableList容器
class HittableList { public: // 自动约束添加的对象必须符合Hittable Concept void add(std::shared_ptr<Hittable auto> obj) { objects.emplace_back(std::move(obj)); } bool hit(const Ray& r, double t_min, double t_max, HitRecord& rec) const { HitRecord temp_rec; bool hit_anything = false; double closest_so_far = t_max; for (const auto& obj : objects) { if (obj.hit(r, t_min, closest_so_far, temp_rec)) { hit_anything = true; closest_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; } private: std::vector<HittableWrapper> objects; };
方案二:std::variant编译期分发(零额外运行时开销)
如果你的可Hittable类型数量固定(比如Sphere、Plane、Triangle等),可以用std::variant存储所有可能的类型,通过std::visit实现编译期分发,完全避免虚函数开销。
步骤1:定义变体类型与访问器
#include <variant> // 假设已有具体的Hittable类型实现 struct Sphere { bool hit(const Ray& r, double t_min, double t_max, HitRecord& rec) const { // 球体相交逻辑 return false; } }; struct Plane { bool hit(const Ray& r, double t_min, double t_max, HitRecord& rec) const { // 平面相交逻辑 return false; } }; // 定义包含所有Hittable类型的变体 using HittableVariant = std::variant<std::shared_ptr<Sphere>, std::shared_ptr<Plane>>; // 实现hit方法的访问器 struct HitVisitor { const Ray& r; double t_min; double& closest_so_far; HitRecord& rec; bool& hit_anything; template<Hittable T> void operator()(const std::shared_ptr<T>& obj) const { HitRecord temp_rec; if (obj->hit(r, t_min, closest_so_far, temp_rec)) { hit_anything = true; closest_so_far = temp_rec.t; rec = temp_rec; } } };
步骤2:实现HittableList容器
class HittableList { public: void add(HittableVariant obj) { objects.push_back(std::move(obj)); } bool hit(const Ray& r, double t_min, double t_max, HitRecord& rec) const { bool hit_anything = false; double closest_so_far = t_max; HitVisitor visitor{r, t_min, closest_so_far, rec, hit_anything}; for (const auto& var : objects) { std::visit(visitor, var); } return hit_anything; } private: std::vector<HittableVariant> objects; };
方案选择建议
- 类型数量固定:优先选
std::variant方案,零额外运行时开销,编译期分发性能最优。 - 类型数量不确定:选自定义类型擦除方案,开销与原虚函数接口完全一致,但无需强制所有业务类继承统一基类,灵活性更高。
内容的提问来源于stack exchange,提问作者Soumen Pradhan
相关产品推荐
相关产品推荐

