C ++中多态对象列表的最佳实践

时间:2009-07-03 18:44:48

标签: c++ polymorphism

存储基类指针列表的常见做法是什么,每个基类指针都可以描述多态派生类?

为了详细阐述并为了一个简单的例子,假设我有一组具有以下目标的类:

  1. 一个抽象基类,其目的是在其派生类上强制执行通用功能。
  2. 一组派生类:可以执行常用功能,本身是可复制的(这很重要),并且可以序列化。
  3. 现在除了这个必需的功能外,我想解决以下要点:

    1. 我希望使用这个系统是安全的;当他/她错误地将基类指针强制转换为错误的派生类型时,我不希望用户有未定义的错误。
    2. 此外,我希望尽可能多地复制/序列化此列表的工作,以便自动处理。原因是,当添加新的派生类型时,我不想搜索许多源文件并确保所有内容都兼容。
    3. 下面的代码演示了一个简单的例子,并且我提出了(我再次寻找一个经常深思熟虑的方法,这样做,我的可能不太好)解决方案。

      class Shape {
      public:
          virtual void draw() const = 0;
          virtual void serialize();
      protected:
          int shapeType;
      };
      
      class Square : public Shape
      {
      public:
          void draw const; // draw code here.
          void serialize(); // serialization here.
      private:
          // square member variables.
      };
      
      class Circle : public Shape
      {
      public:
          void draw const; // draw code here.
          void serialize(); // serialization here.
      private:
          // circle member variables.
      };
      
      // The proposed solution: rather than store list<shape*>, store a generic shape type which
      // takes care of copying, saving, loading and throws errors when erroneous casting is done.
      class GenericShape
      {
      public:
          GenericShape( const Square& shape );
          GenericShape( const Circle& shape );
          ~GenericShape();
          operator const Square& (); // Throw error here if a circle tries to get a square!
          operator const Circle& (); // Throw error here if a square tries to get a circle!
      private:
          Shape* copyShape( const Shape* otherShape );
          Shape* m_pShape; // The internally stored pointer to a base type.
      };
      

      上面的代码肯定缺少一些项目,首先基类会有一个构造函数需要类型,派生类会在构造过程中在内部调用它。此外,在GenericShape类中,将出现复制/赋值构造函数/运算符。

      很抱歉这篇长篇文章,试图完全解释我的意图。在那个注释,并重新迭代:上面是我的解决方案,但这可能有一些严重的缺陷,我很乐意听到他们,以及那里的其他解决方案!

      谢谢

4 个答案:

答案 0 :(得分:5)

std :: list&lt;的问题是什么?形状*&gt; (或者std :: list&lt; boost :: shared_ptr&gt;)?

这将是实现具有多态行为的shape列表的惯用方法。

  
      
  1. 我希望使用这个系统是安全的;当他/她错误地将基类指针强制转换为错误的派生类型时,我不希望用户有未定义的错误。
  2.   

用户不应该向下转换,而应使用提供的多态性和基本(形状)操作。考虑为什么他们会对向下转发感兴趣,如果你找到理由这样做,请回到绘图板并重新设计,以便你的基地提供所有需要的操作。

然后,如果用户想要向下转换,则应使用dynamic_cast,并且它们将获得您尝试在包装器中提供的相同行为(如果向下转换指针或std :: bad_cast异常,则为空指针参考向下转发)。

您的解决方案增加了一个间接级别,并且(使用提供的界面)要求用户在使用前尝试猜测形状类型。您为每个派生类提供了两个转换运算符,但用户必须在尝试使用这些方法之前调用它们(不再是多态的)。

  
      
  1. 此外,我希望尽可能多地复制/序列化此列表的工作,以便自动处理。原因是,当添加新的派生类型时,我不想搜索许多源文件并确保所有内容都兼容。
  2.   

在不处理反序列化的情况下(我将在稍后回来),与在列表中存储(智能)指针相比,您的解决方案需要重新访问适配器,以便为添加到层次结构的每个其他类添加新代码

现在反序列化问题。

建议的解决方案是使用普通的std :: list&lt; boost :: shared_ptr&gt;,一旦你建立了列表,就可以直接执行绘图和序列化:

class shape
{
public:
   virtual void draw() = 0;
   virtual void serialize( std::ostream& s ) = 0;
};
typedef std::list< boost::shared_ptr<shape> > shape_list;
void drawall( shape_list const & l )
{
   std::for_each( l.begin(), l.end(), boost::bind( &shape::draw, _1 ));
}
void serialize( std::ostream& s, shape_list const & l )
{
   std::for_each( l.begin(), l.end(), boost::bind( &shape::serialize, _1, s ) );
}

我使用boost :: bind来减少代码膨胀而不是手动迭代。问题是你不能像构建对象那样虚拟化构造,你不知道它实际上是什么类型。在解决了已知层次结构的一个元素的反序列化问题后,对列表进行反序列化是微不足道的。

此问题的解决方案从未像上面的代码一样简洁明了。

我将假设您已为所有形状定义了唯一的形状类型值,并且您通过打印出该ID来开始序列化。也就是说,序列化的第一个元素是类型id。

const int CIRCLE = ...;
class circle : public shape
{
   // ...
public:
   static circle* deserialize( std::istream & );
};
shape* shape_deserialize( std::istream & input )
{
   int type;
   input >> type;
   switch ( type ) {
   case CIRCLE:
      return circle::deserialize( input );
      break;
   //...
   default:
      // manage error: unrecognized type
   };
}

如果将其转换为抽象工厂,则可以进一步减少处理反序列化函数的需要,在创建新类时,类本身会注册它的反序列化方法。

typedef shape* (*deserialization_method)( std::istream& );
typedef std::map< int, deserialization_method > deserializer_map;
class shape_deserializator
{
public:
   void register_deserializator( int shape_type, deserialization_method method );
   shape* deserialize( std::istream& );
private:
   deserializer_map deserializers_;
};

shape* shape_deserializator::deserialize( std::istream & input )
{
   int shape_type;
   input >> shape_type;
   deserializer_map::const_iterator s = deserializers_.find( shape_type );
   if ( s == deserializers_.end() ) {
      // input error: don't know how to deserialize the class
   }
   return *(s->second)( input ); // call the deserializer method
}

在现实生活中,我会使用boost :: function&lt;&gt;而不是函数指针,使代码更清晰,更清晰,但添加另一个依赖于示例代码。此解决方案要求在初始化期间(或至少在尝试反序列化之前)所有类在 shape_deserializator 对象中注册其各自的方法。

答案 1 :(得分:3)

你可以通过使用模板(对于构造函数和转换器)来避免GenericShape中的大量重复,但缺少的关键位是让它继承自Shape并实现其虚拟化 - 没有它它是无法使用的,它是信封/实现习语的一个非常正常的变种。

您可能希望使用auto_ptr(或更智能的指针),而不是使用Shape的裸指针; - )。

答案 2 :(得分:0)

我会在STL容器中提出boost::shared_pointer<Shape>。然后使用dynamic_cast向下转换保证类型的正确性。如果您想提供辅助函数来抛出异常而不是返回NULL,那么请关注Alex's suggestion并定义模板帮助函数,如:

template <typename T, typename U>
T* downcast_to(U *inPtr) {
    T* outPtr = dynamic_cast<T*>(inPtr);
    if (outPtr == NULL) {
        throw std::bad_cast("inappropriate cast");
    }
    return outPtr;
}    

并使用它:

void some_function(Shape *shp) {
    Circle *circ = downcast_to<Circle>(shp);
    // ...
}

使用像GenericShape这样的单独的类与来自Shape的每个类过于强烈耦合。我想知道这是否会被认为是代码嗅觉 ...

答案 3 :(得分:0)

  

我希望使用这个系统   安全;我不希望用户拥有   他/她时未定义的错误   错误地转换基类指针   错误的派生类型。

为什么会出现未定义的错误? dynamic_cast的行为完全定义良好,如果将基类指针强制转换为错误的派生类型,则会捕获错误。这真的好像重新发明轮子。

  

此外,我想要多少   可能的工作   复制/序列化此列表   照顾自动。该   原因是,作为一个新的派生   添加类型我不想要   搜索许多源文件和   确保一切都会如此   兼容。

我不确定这里的问题是什么。如果所有派生类都是可序列化和可复制的,那还不够好吗?你还需要什么?

我也不确定如何制作前两个要求。 你是什​​么意思,ABC应该“强制执行一个共同的功能”?如果派生类只是为了执行相同的通用功能,可以复制和序列化,那么有什么意义呢?

为什么不将一个非抽象类序列化并可复制呢?

我可能在这里遗漏了一些重要的东西,但我并不认为你已经解释了你想要实现的目标。