【问题标题】:derived class selection based on string - C++基于字符串的派生类选择 - C++
【发布时间】:2014-06-09 11:57:21
【问题描述】:

假设我有类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() 方法。)

而且,更重要的是,有没有更好的方法是我没有想到的?

谢谢。

【问题讨论】:

    标签: c++ inheritance crtp


    【解决方案1】:

    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]();
    

    【讨论】:

    • 我想知道你是否可以完全回避ShapeFactory,并以某种方式获得指向operator new Square()之类的指针。
    • 使用 C++11 lambda:sMap["square"] = [](){ return static_cast&lt;Shape*&gt;(new Square); };
    • @Thomas 将工厂作为一个类有时很有用。一个类可以有数据,这些数据可以传递给构造函数。并且类类型的静态对象有一个构造函数,它可以处理映射中的插入。
    • @Jarod42 谢谢。这绝对是最简单的解决方案 - 它告诉我我真的应该看看 lambda 函数 :-) 如果你把它写成答案,我会接受它;否则,我会接受托马斯的……
    • @Jarod42 顺便说一句,这个解决方案的简单性最接近我的第一个想法,即拥有什么会很好,但(我相信)在 C++ 中是不可能的:从字符串到“形状类型”的映射,所以我可以写类似p2shape = new sMap[shapeName]();
    【解决方案2】:

    要完成@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]();
    

    但最好使用智能指针:

    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]();
    

    【讨论】:

      【解决方案3】:

      一种常见的技术是拥有一个抽象基类构建器,并使映射成为单例。基本构建器的构造函数接受一个字符串,并将一个指向自身的指针以该字符串作为键插入到映射中。每个派生类还创建一个派生构建器(通常是私有的),其构造函数将其类型名称传递给基构建器,其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 函数中 将使用由构造函数初始化的成员变量 论据。

      【讨论】:

      • 这对于我的使用来说是不必要的复杂,但仍然是一个有趣的方法。您能否草拟一下代码,或提供一些示例的链接(因为您称其为常用技术)?
      【解决方案4】:

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

      您可以将其封装在通用工厂函数中。

      使用 CRTP 的草图解决方案尝试是朝着这个方向,但是 CRTP 方面是完全不相关的,不需要复杂化。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-07-05
        • 2014-11-15
        • 2017-04-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多