65.9K
CodeProject 正在变化。 阅读更多。
Home

枚举您的叶子类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (8投票s)

2003 年 9 月 25 日

CPOL

6分钟阅读

viewsIcon

58397

downloadIcon

1734

一篇关于一组宏和一个工厂类,用于枚举和动态创建派生自虚基类的叶子类的文章

引言

MFC 提供了一个强大的框架,只要您从 CObject 派生它们,就可以动态创建类。在我用来作为示例的 ColorFilter 演示项目ような项目中,我开发时,用户需要能够从多个派生类中进行选择。随着项目的演变,这些派生的(叶)类数量不断增加,我发现我必须在程序的各个点进行更改,并且不可避免地总是忘记进行其中一项更改。最初,使用 CObject 和 MFC 的运行时类信息似乎是一个可能的解决方案,但不幸的是,这个框架不能让你区分来自不同(虚拟)基类的叶类,因为 CObject 是 *所有* 类的基类。此外,你*必须*在你的项目中包含 MFC。因此,我开发了一组宏和一个模板工厂类,允许最派生的类(即叶类)自行注册到它所派生的基类。因此,所有叶类都可以通过基类进行枚举*和*创建。

演示项目展示了一种(非常简单的)枚举叶类的方法。许多颜色过滤器都派生自一个基类过滤器。每个叶类都会自动添加到菜单栏,用户可以选择它。选择某个颜色过滤器后,该过滤器会被创建并应用于显示位图的圆形区域。

这种枚举能力带来的真正好处在于,它消除了对每个叶类的硬编码知识的必要性。如果你想添加另一个过滤器,你不需要添加另一个菜单处理程序或更新一个 switch 语句。

背景

虚拟基类经常被用作抽象接口。指向基类的指针足以调用叶类中的函数。

    class CLeaf : public CBase
    {
    };

    SomeGenericFunction(CBase* pBase)
    {
        pBase->WhatEverNeedsToBeDone();
    }

    CBase* pBase = new CLeaf;
    SomeGenericFunction(pBase);
正如这个小示例所示,派生类的硬编码知识是必要的(new CLeaf)。如果你从同一个基类派生了十几个类,并且想实例化其中一个,你的第一个方法可能是像这样写代码:
    CBase* pBase = NULL;
    switch(nDesiredLeafClass)
    {
        case 0: pBase = new CLeaf_0; break;
        case 1: pBase = new CLeaf_1; break;
        case 2: pBase = new CLeaf_2; break;
        .
        .
        .
    }
    SomeGenericFunction(pBase);
现在添加第 13 个叶类会变成一件非常令人头疼的事情。你必须记住在代码的某个地方更新 switch 语句。你总是记得你实现了多少个,以及在哪些文件中吗?如果你想编写健壮的代码,你应该只在一个地方进行更改。

使用代码

实现带工厂的枚举叶类所需的所有代码都包含在一个头文件中:BootStrap.h。将此文件包含在你的项目中,并将宏调用添加到你的基类和叶类中。DECLARE_LEAF_CLASS()IMPLEMENT_LEAF_CLASS() 只需添加到最派生的类中。

演示应用程序中基类的声明

#include "BootStrap.h"

class CBaseFilter  
{
DECLARE_ROOT_CLASS(CBaseFilter)
public:
    void             Apply(...);
    virtual COLORREF ChangeColor(...) = 0;
    .
    .
    .
};

以及这个基类的实现

IMPLEMENT_ROOT_CLASS(CBaseFilter)

void CBaseFilter::Apply(...)
{
   .
   .
   .
   //Call virtual function in leaf class
   COLORREF cr = ChangeColor(crOldPixelColor);
   .
   .
   .
}

IMPLEMENT_ROOT_CLASS(CBaseFilter) 宏会为你的基类添加两个公共的静态函数。它们是:

static int                        GetRegisteredManufactoringPlantCount();
static CBootStrapper<base_class>* GetRegisteredManufactoringPlant(int nIndex);

第一个函数可以让你知道有多少个叶类派生自基类,第二个函数可以让你获取每个叶类的工厂指针。

一个非常简单的派生自这个基类的类声明如下:

#include "BaseFilter.h"

class CBlueFilter : public CBaseFilter  
{
DECLARE_LEAF_CLASS(CBaseFilter)
public:
   CBlueFilter();
   virtual ~CBlueFilter();

   virtual COLORREF    ChangeColor(COLORREF crPure);
};

以及实现如下:

IMPLEMENT_LEAF_CLASS(CBlueFilter, CBaseFilter, _T("Blue Filter"))

CBlueFilter::CBlueFilter(){}
CBlueFilter::~CBlueFilter(){}

COLORREF CBlueFilter::ChangeColor(COLORREF crPure)
{
   COLORREF crNew = RGB(0, 0, GetBValue(crPure));
   return crNew;
}

正如你所看到的,要为类结构添加此功能所需的工作量非常少。而且由于添加到另一个叶类版本的所有必要更改都可以保留在一个文件和一个代码段内,因此复制和粘贴的危险非常小。

第一个示例展示了如何使用添加的功能将叶类的名称添加到主窗口菜单栏的菜单项中。这是通过 CBootStrapper 类中的 GetClassName() 函数实现的。

BOOL CChildView::Create(LPCTSTR lpszClassName, ...) 
{
   .
   .
   .
   // Get the main window's menu
   CMenu* pMenu = pMain->GetMenu();

   //The "Filter" menu should be the second item on the menu bar.
   CMenu* pSubMenu = pMenu->GetSubMenu(1);
   if(NULL != pSubMenu)
   {
      //Append our color filter menu items to the tail of this menu
      CBaseFilter* pBase   = NULL;
      int          nLeaves = pBase->GetRegisteredManufactoringPlantCount();
      UINT         nMenuID = ID_FILTER_NONE + 1;

    //Iterate through the vector of Leaf classes 
    //and add their names as menu items
      for(int nIdx = 0; nIdx < nLeaves; nIdx++, nMenuID++)
      {
         CBootStrapper<CBaseFilter>* pBoot = 
               pBase->GetRegisteredManufactoringPlant(nIdx);
         pSubMenu->AppendMenu(MF_STRING | MF_ENABLED, 
              nMenuID, pBoot->GetClassName());
      }
      // force a redraw of the menu bar
      pMain->DrawMenuBar();
   }
   return TRUE;
}

第二个示例展示了如何使用 CBootStrapper 类中的 CreateObject() 函数创建叶类实例。

BOOL CChildView::OnCmdMsg(UINT nID, int nCode, void* pExtra, 
    AFX_CMDHANDLERINFO* pHandlerInfo) 
{
   UINT nFirstID = ID_FILTER_NONE;
   UINT nLastID  = nFirstID + 
      CBaseFilter::GetRegisteredManufactoringPlantCount();

   //Let the base class handle this command message 
   //if it is not in our range 
   if( (nID < nFirstID) || (nID > nLastID))
   {
      return CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
   }

   //handle a menu command if it is in our ID range and 
   //if it is not a CmdUI message
   if(NULL == pHandlerInfo && CN_COMMAND == nCode)
   {
      //Handle one of our added menu items!!!
      if(nID == nFirstID)
      {
         //No filtering
         m_dcFiltered.BitBlt(0, 0, m_bmp.bmWidth, 
               m_bmp.bmHeight, &m_dcSource, 0, 0, SRCCOPY);
      }
      else
      {
         BeginWaitCursor();

         //Determine the requested filter from the command ID
         int nFilterIndex = nID - nFirstID - 1;
//--- 
         CBootStrapper<CBaseFilter>* pBoot = 
             CBaseFilter::GetRegisteredManufactoringPlant(nFilterIndex);
         //Create an instance of the requested filter
         CBaseFilter* pFilter = pBoot->CreateObject();
         //Do whatever needs to be done
         pFilter->Apply(&m_bmp, &m_dcSource, &m_dcFiltered);
         //And destroy the filter object we've just created
         delete pFilter;
//---
         EndWaitCursor();
      }
      RedrawWindow();
   }

   return TRUE;
}

关注点

静态变量在任何用户代码执行之前就被构造,因此虽然 IMPLEMENT_LEAF_CLASS() 宏依赖于 IMPLEMENT_ROOT_CLASS(xx) 已经被执行,但没有明显的方法可以在用户代码中强制执行这一点。根据 MSDN 库,你唯一能获得的保证是静态对象按照它们在源文件中出现的顺序被构造,但不能保证源文件被链接的顺序!因此,基类及其派生类中的所有静态对象似乎应该在同一个源文件中,这样它们的顺序就可以确保正确初始化。这种情况非常令人恼火,因为它阻止了使用基类和派生类的独立实现文件。

起初,我希望通过将静态代码的实现集中在模板函数中来解决这个问题,叶类只需要从基类和模板类派生。VC++ 6.0 链接器中的一个 bug 阻止了这一点,因为它找不到模板类中静态函数(错误:LNK2001)的代码。我的第二个想法是在宏函数中放置一些编译器指令,如果 IMPLEMENT_LEAF_CLASS(xx) 没有出现在与 IMPLEMENT_ROOT_CLASS(xx) 宏相同的文件中,则发出警告。好吧,宏函数不允许使用编译器指令(谁能想到!可以通过一些简单的宏来避免这么多愚蠢的 bug)。

在 MSDN 数据库中寻找可能的解决方案时,我遇到了 #pragma init_seg() 编译器指令。使用它可以强制编译器将你的静态对象的构造函数(当然还有析构函数)放入特殊的初始化段中。位于“compiler”段中的对象在位于“lib”段中的对象*之前*被构造,而“lib”段中的对象在位于“user”段中的代码之前被构造。默认情况下,你编写的代码被放在“user”段中。因此,通过将基类中工厂向量的构造函数放在“lib”段中,可以确保在叶类中的工厂构造函数将自己添加到这个向量*之前*被初始化!

#pragma init_seg() 按预期工作,并且我在宏集的第一版中使用了它。但正如 PeterH 恰当地指出的那样,编译器指令不是可移植的。而且,尽管我尽了最大的努力让一切都保持在单个语句中,但需要添加这个语句,并且没有任何方法可以强制其存在或检测其缺失,这仍然不能做到万无一失。因此,基于 PeterH 的评论,我研究了使用 C++ 标准来解决这个问题的可能性。现在,IMPLEMENT_ROOT_CLASS(xx) 宏中一个看起来有点傻的语句会强制编译器在第一个叶类注册其静态工厂类时创建基类中的静态向量。

unsigned int base_class::RegisterManufactoringPlant(
    CBootStrapper<base_class>* pLeaf)
{                    
/*!!*/
  static bool blCreate = (NULL == 
    (m_vecRegisteredLeafManufactoringPlants = 
     new std::vector<CBootStrapper<base_class>*>() )); 
  m_vecRegisteredLeafManufactoringPlants->push_back(pLeaf);    
    return GetRegisteredManufactoringPlantCount();  
}  

历史

  • 创建于 03 年夏末
  • 于 03 年 12 月根据 PeterH 的建议改进
© . All rights reserved.