假设我有类Square
和Circle
,它们都来自Shape
,而Shape * p2shape
指针应该得到一个新类型的对象,其类型由{string shapeName
决定1}}。
目前,我使用以下方法:
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()
个方法。)
而且,更重要的是,还有一些我没有想过的更好的方法吗?
感谢。
答案 0 :(得分:2)
Shape_CRTP
模板基本上是形状的 factory ,因此我将其命名为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]();
答案 1 :(得分:1)
一种常见的技术是使用抽象基类构建器,并使地图成为单例。基础构建器的构造函数接受一个字符串,并将一个指针插入自身,并将该字符串作为键插入到映射中。每个派生类还创建一个派生构建器(通常是私有的),其构造函数将其类型名称传递给基础构建器,并且其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
将使用使用构造函数初始化的成员变量
参数。
答案 2 :(得分:1)
要完成@Thomas's answer,您可以在C ++ 11中使用:
std::map<string, std::function<Shape*()>> sMap;
sMap["square"] = [](){ return new Square; };
sMap["circle"] = [](){ return new Circle; };
p2shape = sMap[shapeName]();
答案 3 :(得分:0)
您可以使用类特定工厂函数的映射,而不是类标识符值的映射。
您可以将其封装在一般工厂功能中。
使用CRTP的草图解决方案尝试是朝这个方向发展的,但CRTP方面完全不相关,这是一个不需要的复杂功能。