基于string的派生类选择

derived class selection based on string - C++

本文关键字:选择 派生 string 基于      更新时间:2023-10-16

假设我有SquareCircle类,它们都是从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;

,并在静态的定义中传递类型的名称例如变量。或者如果你有几个关键字解析到相同的类,但使用不同的初始化式可以为它创建一个构建器,使用关键字和初始化器作为参数,newbuild函数中会使用构造函数初始化的成员变量吗参数。

可以使用特定于类的工厂函数的映射,而不是类标识符值的映射。

你可以把它封装在一个通用工厂函数中。

使用CRTP的草图解决方案尝试是在这个方向上,但CRTP方面是完全无关的,一个不必要的复杂性。