C++单例设计:使用继承只调用一些实现的方法

Posted

技术标签:

【中文标题】C++单例设计:使用继承只调用一些实现的方法【英文标题】:C++ singleton design: using inheritance to call only some implemented methods 【发布时间】:2010-12-18 09:19:50 【问题描述】:

我有一个单例,它是我的游戏的主要引擎容器。 我有几个抽象类,可以让开发人员实现对特定对象所需的不同调用。

我想做的是让我的单例在每个给定对象上调用这些方法,但避免不必要的调用。

示例以便您更好地理解: 想象一个同时需要 Render() 和 Update() 方法的对象。

class IRender() : public IBase

  virtual bool Render() = 0;
;

class IUpdate() : public IBase

  virtual bool Update( long time_delta ) = 0;
;

class Sprite : public IRender, public IUpdate

  bool Render() render stuff; 
  bool Update( long time_delta() update stuff; 
;

现在我想将该 Sprite 对象添加到我的单例引擎中,但我希望引擎仅调用每个循环来调用该对象继承的任何内容(渲染和更新之外还有其他内容,例如检查输入等):

_engine::getInstance()->Add( _sprite );

事情是,为了让它工作,我必须从一个基接口继承所有接口,这样我就可以用任何创建的对象调用 Add(),所以 Add() 方法接收一个基接口对象。

现在这里的问题是基本接口必须至少抽象出所有可以继承的方法,如 Render()、Update()、CheckInput()、Etc(),并且我的单例的每个循环都必须调用所有可能性对于每个对象,即使对于 sprite 类来说 CheckInput() 为空。 就像我的单身人士一样:

bool loop()
  for every object saved in my container:
    CheckInput(); Update(); Render(); etc(); 

我的设计错了吗?有什么办法可以避免这种情况吗? 我希望你明白我的意思,我的引擎处于相当先进的状态,我正在努力避免全部重写。

提前致谢。

【问题讨论】:

为什么不让引擎存储多个集合,每个接口类型一个集合 - vector 等? 【参考方案1】:

如果您不需要在移动到下一个对象之前对每个对象调用链 CheckInput(), Update(), ....,请调用该对象,并使对象注册哪个它在单例上支持的操作。

class Singleton 
  void Add(Base& object) 
    object.register(this);
  

  void registerForInput(IInput& object) 
    addToInputList(object);
  


class Some : public Base, public IInput 
  void register(Singleton *target) 
    target->registerForInput(*this);
  

但是,如果您需要在移动到下一个对象之前在一个对象上调用所有这些方法,请在该对象上创建一个方法来执行它支持的所有调用。

class Singleton 
  void loop() 
    for all objects:
      object.performActions();
  


class Some : public Base 
  void performActions() 
    checkForInput();
    render();
  

【讨论】:

很好,不知道双重调度,它完全解决了我想要的问题。我的设计也是如此,因此在每个对象上都调用了 update(),然后是 render() 等,所以它就像一个魅力。再次感谢:) 你真的需要这个解决方案(即一次性执行给定对象的所有方法)吗?我使用了另一种想法>>一次性对所有对象执行一种方法,这更加优雅,因为它不会给接口类的用户带来太多负担。 @Rickard - 你能解释一下为什么这被认为是双重调度吗?这对我来说似乎很简单。 @Aaron - 你说得对,这不是双重派遣。我首先要通过更改它来进行真正的双重调度。我会改变的。 这个单例不支持多种类型?此外,所有单身人士都需要了解所有类型?还是我错过了什么?【参考方案2】:

如果你的基接口定义了太多纯虚函数,那么每个派生类都必须定义它们。 我会在IBase 中创建单个纯虚函数,我们将其称为EngineCommunication,在派生类中将调用CheckInput(); Update(); Render() etc.(特定于类) 你的引擎会在循环中调用EngineCommunication,让具体的类决定做什么。

【讨论】:

再次阅读问题,IBase 声明所有方法 virtual 但不是 pure(可能什么都不做 impl),否则 IRender 不会将方法重新声明为纯方法以强制实施。 那么IBase不能归类为接口类(虽然概念上是这样)。无论如何 - 这是次要的,不会影响我的建议。【参考方案3】:

至少可以说,将所有方法从子类推送到基类似乎很奇怪。为什么不尝试使用您要调用的方法将每个对象动态转换为接口 - 如果转换返回 nullptr 则跳过它:

bool loop()

   for every object saved in my conatiner:
       IRender* render = dynamic_cast<IRender*> (object_ptr);
       if (render != 0)
           render->Render ();

       // etc.

或者从子类中的单个方法调用所有操作 - 例如执行()。您可能需要为您的子类使用虚拟继承才能只有一个 IBase 基。

【讨论】:

确实可以,但看起来有点难看:p 我也不知道是否对每个对象(可能是数千个)进行强制转换后跟一个 if 语句每个循环都是一个很好的 ideia 性能明智,但感谢您的帮助:) 我在 addObject() 中使用“dynamic_cast”来检查它是否匹配更新列表(否则无法添加),然后在循环中避免它。 我没有看到演员表的问题 - 从语义上讲,我将其解读为“如果这个对象是可渲染的,那么就渲染它”。似乎比将基类与所有子类方法的超集混在一起更可取。 我同意混乱的基类是不好的,特别是因为向集合添加接口最终会导致 bigbang 重新编译......但dynamic_cast 通常是糟糕设计的标志,因为你应该永远不必向上投射对象。所以在仅仅因为它存在而盲目依赖之前,最好先停止思考,看看如何避免它。【参考方案4】:

如果你想写类似的东西

bool loop()

    foreach IBase _base in _container do
    
         _base->CheckInput();
         _base->Update();
         _base->Render();
         ...
    

您的所有子类都必须实现每个方法。如果您不希望最终类(即Sprite)实现所有方法,您可以在子类中将这些方法实现为空,然后使用virtual inheritance。但它会变得有点复杂。比如:

bool loop()

    foreach IBase _base in _container do
    
         IChecker * _checker = dynamic_cast<IChecker *>(_base);
         if (_checker != 0)
             _checker->CheckInput();

         IUpdater * _updater = dynamic_cast<IUpdater *>(_base);
         if (_updater != 0)
             _updater->Update();

         IRender * _render = dynamic_cast<IRender *>(_base);
         if (_render != 0)
             _render->Render();
         ...
    


class IBase

    virtual bool Render() = 0;
    virtual bool CheckInput() = 0;
    virtual bool Update() = 0;
    virtual bool Render() = 0;
    ...


class IRender : public virtual IBase

    virtual bool Render()
    
         // do stuff
    

    virtual bool CheckInput()
    
        return false;
    

    ...

它很复杂,而且你重复代码一无所获。另外,我不确切知道dynamic_cast 的性能损失,但肯定有。我希望你可以用这个想法来保存你的代码。

【讨论】:

您不需要同时拥有基类中的所有方法都使用dynamic_casts - 一个或另一个就足够了。 我知道,这只是为了避免在最终类中定义方法。您可以这样做或在基础 IBase 中定义所有空方法。【参考方案5】:

对我来说这听起来像是早期的优化 - 你确定你真的需要避免这些调用吗?通常,与将像素实际推送到屏幕上的工作相比,空 Render() 调用的开销非常小,不值得优化。

但是 - 如果你真的想避免这些电话,你可以这样做:

class IBase

  unsigned int mask;
  const unsigned int DONT_CALL_MYFUNCTION = 1;

  IBase() : mask(0)  

  virtual void MyFunction()  mask |= DONT_CALL_MYFUNCTION; 



...

if((obj->mask & DONT_CALL_MYFUNCTION) == 0)
  obj->MyFunction();

您最终会调用每个基本函数一次 - 但是一旦基本实现意识到它没有被覆盖,那么它将停止调用虚函数。

但又一次 - 我怀疑这是否值得。

【讨论】:

【参考方案6】:

肮脏的解决方案,首先:)

只需在接口上添加一个“标签”。显然它们已经绑定在一起了,所以:

class IBase  public: void addTag(Tag t); const TagLists& getTags() const; 

IRender::IRender() : IBase()  addTag(IRender::Tag); 

然后您可以只要求“标签”并使用开关进行调度:

const TagList& tags = base->getTags();
for(TagList::const_iterator it = tags.begin(), end = tags.end(); it != end; ++it)

  switch(*it)
  
  case IRender::Tag:
    base->render();
    break;
  case IUpdate::Tag;
    base->update();
    break;
  

嗯,正如我所说,很漂亮不是吗?

我唯一的问题是:你真的需要这样做吗?

显然这有点尴尬...我的意思是所有这些方法都在闲逛...

唯一的问题是你想如何进行:

对于每个对象,按一定顺序调用所有方法 对于每个方法,在所有对象上调用它

以对象为中心

我不知道如何正确地做到这一点:总结它们的go 方法最终会得到很多参数:/

那么你需要掩码,或者注册...

以方法为中心

一个包含对象的单例(如果有必要的话),一个单例RenderSingleton 包含一个指向从Render 派生的每个对象的指针。

class Render

public:
  Render() : Base()  RenderSingleton::Add(this); 
  ~Render()  RenderSingleton::Remove(this); 
;

class RenderSingleton

public:
  static void Add(Render* i);
  static void Remove(Render* i);

  static void Render(....); // calls on each obj with specific render parameters
;

这里的主要优点是参数是特定于render方法的,不需要为check等传递参数...促进解耦。

另外,请注意,如果您添加一个新接口,它独立于其他接口。

如果可以的话,去吧。

【讨论】:

顺便说一下,这就是为什么它被显示为脏的原因 :)【参考方案7】:

我通常做的是定义一个带有模板子类的侦听器对象,该子类执行dynamic_cast 并将其传递给“更新程序”,然后在添加/删除正确类型的对象时调用它。

class ObjectListenerBase 

private:
    friend class ObjectManager;

    void onAdded(ScriptObject* obj)  doOnAddedBase(obj); 
    void onRemoved(ScriptObject* obj)  doOnRemovedBase(obj); 

    virtual void doOnAddedBase(ScriptObject* obj) = 0;
    virtual void doOnRemovedBase(ScriptObject* obj) = 0;
;

template<class T>
class ObjectListener : public ObjectListenerBase

protected:
    virtual void doOnAdded(T* obj) = 0;
    virtual void doOnRemoved(T* obj) = 0;

private:
    void doOnAddedBase(ScriptObject* obj)  if (T* o = dynamic_cast<T*>(obj))  doOnAdded(o);  
    void doOnRemovedBase(ScriptObject* obj)  if (T* o = dynamic_cast<T*>(obj))  doOnRemoved(o);  
;

//// how add object might look like

void ObjectManager::addObject(const std::string& object_name, ScriptObjectPtr s_object)

    ....

    for_each(m_listeners, boost::bind(&ObjectListenerBase::onAdded, _1, s_object.get()));


//// how a listener might look like
class AIEngine : public ObjectListener<IThinker>

public:
    AIEngine()  ObjectManager::the().addListener(this); 

    void think()  for_each(m_ais, boost::bind(&IThinker::think, _1, ...));

protected:
    virtual void doOnAdded(IThinker* obj)  m_ais.push_back(obj) 
    virtual void doOnRemoved(IThinker* obj)  m_ais.erase(obj); 

private:
    ptr_set<IThinker> m_ais;
 

如果你愿意,你可以为不同的类型多次继承 ObjectListener。

【讨论】:

有趣地使用dynamic_cast,一次意味着没有太多的性能损失。但是使用模板,你可以完全避免它。 如何通过使用模板完全避免它? (注意,我希望将内容分离到 h/cpp 中,这样我就不必 #include 其他所有内容。)【参考方案8】:

Evolve your hierarchy,使用component-based game object system。它由 Scott Bilas 在 2002 年提出并实现了最早的 OOP 原则之一:优先组合而不是继承。这种方法现在很流行。

【讨论】:

以上是关于C++单例设计:使用继承只调用一些实现的方法的主要内容,如果未能解决你的问题,请参考以下文章

设计模式C++实现单例模式

设计模式C++实现单例模式

设计模式-单例模式

构造析构;重写;设计模式;单例;抽象;重载

C++设计模式:单例模式

设计模式