C++基类向量管理派生滤镜类,如何无类型转换设置参数?
问题场景
现有多个图像处理滤镜类F1、F2等,均继承自抽象基类Filter并实现了虚方法run()。使用vector<unique_ptr<Filter>>存储这些滤镜实例时,遍历调用run()可以正常工作,但无法直接通过基类指针调用派生类的专属参数设置方法(如F1::param1()、F2::param2()),且不想通过dynamic_cast这类类型转换操作来实现参数设置。需要更合理的设计方案解决该问题。
当前代码示例:
// g++ test.cpp && a.out #include <iostream> #include <string> #include <vector> #include <memory> class Filter { public: Filter(void){ std::cout << "Filter::Filter() : called" << std::endl; } virtual void run(int x) = 0; }; class Filters { public: std::vector<std::unique_ptr<Filter>> f; }; class F1 : public Filter { public: void run(int x){ std::cout << "F1::run(): p1=" << myparam1 << std::endl; } void param1(int x){ myparam1 = x; } int param1(void){ return myparam1; } private: int myparam1 = 12; }; class F2 : public Filter { public: void run(int x){ std::cout << "F2::run(): p2=" << myparam2 << std::endl; } void param2(double x){ myparam2 = x; } double param2(void){ return myparam2; } private: double myparam2 = 11.0; }; int main(void){ Filters fs; fs.f.push_back(std::make_unique<F1>()); // 编译失败:基类指针无法调用派生类专属方法 //fs.f.back()->param1(200); fs.f.push_back(std::make_unique<F2>()); // 只能通过类型转换调用,不符合需求 dynamic_cast<F2*>(fs.f.back().get())->param2(100.0); for(std::unique_ptr<Filter> &f : fs.f){ f.get()->run(10); } }
优化设计方案
方案1:实例创建时直接配置参数
如果参数值在创建滤镜实例时就能确定,无需等到存入容器后再设置,可以在创建时直接调用参数方法,通过链式调用实现(需要修改参数方法返回自身指针)。
修改派生类的参数方法:
class F1 : public Filter { public: // 修改param1返回F1*,支持链式调用 F1* param1(int x){ myparam1 = x; return this; } // ... 其他代码不变 }; class F2 : public Filter { public: // 修改param2返回F2*,支持链式调用 F2* param2(double x){ myparam2 = x; return this; } // ... 其他代码不变 };
创建实例时直接设置参数:
int main(void){ Filters fs; // 创建F1时直接设置参数,再存入容器 fs.f.push_back(std::unique_ptr<Filter>(std::make_unique<F1>()->param1(200))); fs.f.push_back(std::unique_ptr<Filter>(std::make_unique<F2>()->param2(100.0))); for(auto &f : fs.f){ f->run(10); } }
优点:实现简单,无需修改基类,完全避免类型转换。
缺点:仅适用于参数在实例创建阶段就能确定的场景。
方案2:基类添加通用参数设置接口
通过在基类中定义一个通用的参数设置虚方法,派生类根据自身参数类型重写该方法,利用std::any实现类型擦除,统一处理不同类型的参数设置。
首先修改基类并引入std::any:
#include <any> #include <string> class Filter { public: virtual ~Filter() = default; // 添加虚析构函数,避免内存泄漏 virtual void run(int x) = 0; // 通用参数设置接口:返回是否设置成功 virtual bool set_param(const std::string& key, const std::any& value) = 0; };
派生类重写set_param方法:
class F1 : public Filter { public: // ... 原有代码不变 bool set_param(const std::string& key, const std::any& value) override { if (key == "param1") { // 尝试将value转换为int类型 if (auto val_ptr = std::any_cast<int>(&value)) { myparam1 = *val_ptr; return true; } } return false; // 参数key不匹配或类型错误 } }; class F2 : public Filter { public: // ... 原有代码不变 bool set_param(const std::string& key, const std::any& value) override { if (key == "param2") { // 尝试将value转换为double类型 if (auto val_ptr = std::any_cast<double>(&value)) { myparam2 = *val_ptr; return true; } } return false; } };
使用时直接通过基类指针调用set_param:
int main(void){ Filters fs; fs.f.push_back(std::make_unique<F1>()); // 无需类型转换,直接设置参数 fs.f.back()->set_param("param1", 200); fs.f.push_back(std::make_unique<F2>()); fs.f.back()->set_param("param2", 100.0); for(auto &f : fs.f){ f->run(10); } }
优点:统一接口,无需知道具体派生类类型即可设置参数,灵活性高。
缺点:需要处理类型安全问题(转换失败时返回false),运行时才会发现类型错误。
方案3:使用访问者模式
如果需要对不同滤镜执行多种复杂操作(不止参数设置),可以采用访问者模式,通过双分派机制让基类指针调用派生类的专属方法。
首先定义访问者基类:
class FilterVisitor { public: virtual ~FilterVisitor() = default; // 针对每个派生类定义访问方法 virtual void visit(F1& filter) = 0; virtual void visit(F2& filter) = 0; };
修改基类添加accept方法:
class Filter { public: virtual ~Filter() = default; virtual void run(int x) = 0; virtual void accept(FilterVisitor& visitor) = 0; };
派生类实现accept方法:
class F1 : public Filter { public: // ... 原有代码不变 void accept(FilterVisitor& visitor) override { visitor.visit(*this); } }; class F2 : public Filter { public: // ... 原有代码不变 void accept(FilterVisitor& visitor) override { visitor.visit(*this); } };
实现参数设置的访问者:
#include <any> #include <string> class ParamSetter : public FilterVisitor { private: std::string param_key; std::any param_value; public: ParamSetter(std::string key, std::any value) : param_key(std::move(key)), param_value(std::move(value)) {} void visit(F1& filter) override { if (param_key == "param1") { if (auto val_ptr = std::any_cast<int>(¶m_value)) { filter.param1(*val_ptr); } } } void visit(F2& filter) override { if (param_key == "param2") { if (auto val_ptr = std::any_cast<double>(¶m_value)) { filter.param2(*val_ptr); } } } };
使用访问者设置参数:
int main(void){ Filters fs; fs.f.push_back(std::make_unique<F1>()); ParamSetter setter1("param1", 200); fs.f[0]->accept(setter1); fs.f.push_back(std::make_unique<F2>()); ParamSetter setter2("param2", 100.0); fs.f[1]->accept(setter2); for(auto &f : fs.f){ f->run(10); } }
优点:扩展性强,后续新增滤镜或操作时只需添加对应的访问方法,无需修改现有滤镜类。
缺点:代码量较大,新增派生类时需要同步修改访问者基类和所有访问者实现。
关键说明
编译期无法自动识别vector<unique_ptr<Filter>>中元素的实际派生类型——因为容器存储的是基类指针,类型信息仅在运行时可用。上述方案均通过运行时多态机制实现无需显式类型转换的参数设置,避免了dynamic_cast带来的耦合和潜在风险。
内容的提问来源于stack exchange,提问作者bliako

