基于string的派生类选择
derived class selection based on string - C++
假设我有Square
和Circle
类,它们都是从Shape
派生的,还有一个Shape * p2shape
指针,它应该得到一个由string shapeName
决定类型的新对象。
目前,我正在使用以下方法:
enum class Shapes {square, circle};
std::map<string, Shapes> sMap;
sMap["square"] = Shapes::square;
sMap["circle"] = Shapes::circle;
switch (sMap[shapeName]) {
case Shapes::square:
p2shape = new Square();
break;
case Shapes::circle:
p2shape = new Circle();
break;
}
这样做的缺点是添加新的派生类需要在三个额外的地方进行更改:
- 添加新项目到类
- 添加类到地图
- 将类添加到
switch
我决定找到一个更简单的解决方案,最终有两个版本,这两个版本都避免使用switch
命令,通过使用指针来创建新对象的静态函数:
class Square : public Shape {
public:
static Shape * create() { return new Square(); }
};
class Circle : public Shape {
public:
static Shape * create() { return new Circle(); }
};
std::map<string, Shape * (*) ()> sMap;
sMap["square"] = Square::create;
sMap["circle"] = Circle::create;
p2shape = sMap[shapeName]();
这意味着新的派生类只需要在一个额外的地方进行更改,即映射。此外,每个派生类必须具有静态create()
方法。经过一些额外的搜索,我发现我可以使用CRTP来摆脱后一种需求,但代价是额外的复杂性:
template <class DerShapeT>
class Shape_CRTP : public Shape {
public:
static Shape * create() { return new DerShapeT(); }
};
class Square : public Shape_CRTP<Square> {};
class Circle : public Shape_CRTP<Circle> {};
std::map<string, Shape * (*) ()> sMap;
sMap["square"] = Square::create;
sMap["circle"] = Circle::create;
p2shape = sMap[shapeName]();
由于我从未使用过(甚至没有听说过)CRTP,我想问一下这种方法是否有一些缺点?(好处是不需要在所有派生类中使用create()
方法。)
更重要的是,有没有更好的方法是我没有想到的?
谢谢。
Shape_CRTP
模板本质上是形状的工厂,所以我将其命名为ShapeFactory
。并且它不需要从Shape
本身继承;您可以将工厂与形状解耦。然后你会注意到工厂只是一个没有任何状态的单个函数的包装器,所以我们可以使用函数模板来代替。
typedef Shape* (*ShapeFactory)();
template<class ShapeT>
Shape *newShape() {
return new ShapeT();
}
class Square : public Shape {};
class Circle : public Shape {};
std::map<string, ShapeFactory> sMap;
sMap["square"] = &newShape<Square>;
sMap["circle"] = &newShape<Circle>;
p2shape = sMap[shapeName]();
要完成@Thomas的回答,您可以在c++ 11中使用:
std::map<string, std::function<Shape*()>> sMap;
sMap["square"] = [](){ return new Square; };
sMap["circle"] = [](){ return new Circle; };
p2shape = sMap[shapeName]();
但是使用智能指针更好:
std::map<string, std::function<std::unique_ptr<Shape>()>> sMap;
sMap["square"] = []() -> std::unique_ptr<Shape> { return std::make_unique<Square>(); };
sMap["circle"] = []() -> std::unique_ptr<Shape> { return std::make_unique<Circle>(); };
p2shape = sMap[shapeName]();
一种常用的技术是使用抽象基类构建器,并使映射成为单例。基构造器的构造器接受一个字符串,并将一个指向自身的指针(以该字符串为键)插入到映射中。每个派生类还创建一个派生构造器(通常是私有的),其构造器将其类型名称传递给基构造器,并且其build
函数返回正确类型的实例。实际的类还定义了这个派生的构造器的静态实例。
这样做的好处是,您可以随时添加派生类,而无需修改任何公共代码。实际上,您可以将每个派生类放在单独的DLL中,在运行时显式加载,并识别和构建在编译基类和公共代码时甚至不存在的派生类。或者从配置文件中选择要加载哪些dll,从而选择要支持哪些类。
缺点是需要更多的输入。这可以通过将具体构建器作为模板类来部分抵消,并且可以通过使用宏(假设它们不会吓到您)来更多地抵消。但是它比其他一些解决方案更复杂,因此应该只在额外的灵活性有用时才使用。
编辑:还有一点:当将工厂插入到映射中时,应该在映射上使用insert
,而不是[]
操作符。您想要测试插入是否成功;如果已经有相同名称的条目,它将失败([]
将简单地覆盖它)。
举个例子:
class Shape
{
private:
class AbstractBuilder;
typedef std::map<std::string, AbstractBuilder const*> BuilderMap;
static BuilderMap ourBuidlerMap;
protected:
class AbstractBuilder
{
protected:
~AbstractBuilder() = default;
AbstractBuilder( std::string const& typeName )
{
if ( !Shape::ourBuilderMap.insert( std::make_pair( typeName, this ) ).second ) {
// Some sort of fatal error... or an exception
}
}
public:
virtual Shape* build() const = 0;
};
public:
static Shape* build( std::string const& typeName )
{
BuilderMap::const_iterator builder = ourBuilderMap.find( typeName );
return builder == ourBuilderMap.end()
? nullptr
: builder->build();
}
};
和在每个派生类中:
class Square : public Shape
{
private:
class Builder : public Shape::AbstractBuilder
{
public:
Builder() : Shape::AbstractBuilder( "square" ) {}
Shape* build() const { return new Square; }
}
static Builder ourBuilder;
// ...
};
当然,您必须为每个实例提供一个实际的实例静态对象。你可能想不想要建筑商,等被嵌套。有很多变体:你可以创建一个用于Shape类中派生构建器的模板,然后只写:
static Shape::ConcreteBuilder<Square> ourBuilder;
,并在静态的定义中传递类型的名称例如变量。或者如果你有几个关键字解析到相同的类,但使用不同的初始化式可以为它创建一个构建器,使用关键字和初始化器作为参数,new
在build
函数中会使用构造函数初始化的成员变量吗参数。
可以使用特定于类的工厂函数的映射,而不是类标识符值的映射。
你可以把它封装在一个通用工厂函数中。
使用CRTP的草图解决方案尝试是在这个方向上,但CRTP方面是完全无关的,一个不必要的复杂性。
- 如何使用默认参数等选择模板专业化
- 为什么使用 "this" 指针调用派生成员函数?
- 具有奇怪重复模板模式的派生类中的成员变量已损坏
- 如何(从固定列表中)选择一个数字序列,该序列将与目标数字相加
- 在派生函数中指定void*参数
- 如何通过派生类函数更改基类中的向量
- C++ 将派生类的成员函数指针作为参数传递时选择了错误的模板专用化
- 选择专用于派生实例的基类的类模板
- C++:使用重载而不是动态强制转换通过基选择派生类
- 施法派生**→基地**有错吗?还有什么选择?
- 选择要在编译时使用的派生类
- 根据函数参数选择派生类
- 隐藏代码以选择接口类的派生类
- 如何在派生CComboBox的类中捕获新的项选择
- 为什么在返回从函数的返回类型派生的类型本地对象时不选择 move 构造函数?
- 派生对象的static_cast是否总是选择大多数派生对象
- 基于string的派生类选择
- 根据ID值选择正确的派生类
- 如何让编译器为派生类选择方法的非模板版本
- 选择要从中派生的 CRTP 基类