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

使用 boost::bind 动态分配和存储带有返回值处理器的操作序列

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (5投票s)

2011年2月24日

CPOL

9分钟阅读

viewsIcon

38122

downloadIcon

226

一个模块,用于动态分配、存储和调用具有不同返回值处理器的函数对象:函数对象、函数、成员函数。

引言

我想向您提供一个(我认为)不错的解决方案,以解决一个特定问题。假设您必须让您的程序用户能够动态分配任务,让程序来执行。或者您想编写一个可编程的测试模块——例如,您希望能够以任何顺序定义执行任务的任何序列。当然,您可以编写几个具有相同接口的类:编写一个抽象基类,继承几个不同的子类,然后创建一个指向它们的对象的基类指针列表,以调用相同的虚拟接口函数。但您必须继承它们,这意味着——您必须实现很多可能的事情。这至少会耗费时间。让我们从另一个角度来看待这个问题。在现实生活中,如果您想修理或制作某件东西,您会逐一拿起所需的工具,然后做您想做的事情。为什么不创建一个像我们的手(当然还有大脑)一样的工具,它可以封装任何特定的现有工具并使用它呢?我提出了两个类,它们将封装任何功能,还有一个类可以存储这些封装的任何序列。现在来谈细节。(要使用本文的代码,您必须知道如何在计算机上安装 boost 或在项目属性的附加包含目录中设置它。)

使用代码

封装器

//(DynamicActionSequence.cpp)
class ActionInterface
{
public:
virtual ~ActionInterface(){}
      virtual void DoAcion() = 0;
};
 
1.    template<typename Action>
2.    class ActionHolder : public ActionInterface
3.    {
4.          Action m_Action;
5.    public:
6.          ActionHolder(Action act) : m_Action(act)
7.          {
8.          }
9.          virtual void DoAcion()
10.         {
11.               m_Action();
12.         }
13.   };

ActionInterface 定义了必须在子类中重写并调用的接口函数。它的子类 ActionHolder 使用两种多态性

  1. 它继承自接口——因此,指向它的指针可以与其他可能的子类的指针存储在同一个容器中,以便调用相同的 DoAction();
  2. 它是一个模板类——因此它可以存储在任何函数对象中,并由 boost::bind 返回。

如果您已经使用过 boost::bind,则可以跳过此段。

这个工具可以将最多 9 个参数的函数或函数对象绑定在一起,并返回一个函数对象,该函数对象可以简单地用空括号像这样调用

11.    m_Action();

这是 boost 文档中的一段摘录

////////////////////
int f(int a, int b)
{
    return a + b;
}

int g(int a, int b, int c)
{
    return a + b + c;
}

bind(f, 1, 2) 将生成一个“空元”函数对象,该对象不接受任何参数,并返回 f(1, 2)。同样,bind(g, 1, 2, 3)() 等同于 g(1, 2, 3)

但是,每次为不同的源(函数或函数对象)调用 boost::bind 时,它都会返回一个全新的函数对象类型。因此,为了存储未知类型,类 ActionHolder 和成员 storage 必须被声明为模板类型。

4.          Action m_Action;

结果,ActionHolder 可以存储任何函数对象,并且本身可以存储在容器中。

将操作添加到操作容器中

让我们看看这段指令做了什么

dash.AddActionToSequence(boost::bind<int>(A<int>(), 2,3));

A<int>() 创建一个 A<int> 类的对象。根据这个类的声明

template<typename T>
class A
{
public:
      T operator ()(T a, T b)
      {
            cout<<"Inside class A T operator ()(T a, T b)"<<endl;
            return a * b;
      }
};

它有一个重载的 operator (),它接收两个参数。要调用它,我们必须传递两个参数。

A<int> a;
A(2,3);

boost::bind<int>(A<int>(), 2,3) 创建一个函数对象,该对象作为参数传递给 dash.AddActionToSequence() 调用。因此,它将被存储为新创建的 ActionHolder 对象中的 m_Action(上面第 4 行)(参见 DynamicActionSequenceHolder 类的以下定义,特别是 AddActionToSequence 模板成员函数的声明和主体)。最后,它将被调用为 m_Action();——根本不传递任何参数。

关键点:接口函数 DoAction 在不传递参数的情况下调用 m_Action(),因此我们也必须调整所有存储的功能(函数、对象)以便在没有参数的情况下通过此接口指令调用它们。

ActionSequenceHolder

DynamicActionSequenceHolder 不言自明

class DynamicActionSequenceHolder
{
      vector<ActionInterface *>  m_ActionsArr;
public :
      //DynamicActionSequenceHolder(){}
      ~DynamicActionSequenceHolder()
      {           
            int iSize = (int)m_ActionsArr.size();
            for(int i = 0; i < iSize; ++i)
                  delete m_ActionsArr[i];

      }
      template<typename Action>
      void AddActionToSequence(Action act)
      {
            m_ActionsArr.push_back(new ActionHolder<Action>(act));
      }
      void ActionSequence()
      {
            int iSize = (int)m_ActionsArr.size();
            for(int i = 0; i < iSize; ++i)
                  m_ActionsArr[i]->DoAcion();
      }
};

让我们看一下

template<typename Action>
void AddActionToSequence(Action act)
{
      m_ActionsArr.push_back(new ActionHolder<Action>(act));
}

这个模板函数使用从接收的参数中进行的模板运行时类型推导,这在模板类的构造函数中无法做到——因此它的作用是:推导 boost::bind 返回的函数对象类型,并在此特定类型上创建 ActionHolder

Main 简单地创建一个 DynamicActionSequenceHolder 对象,向容器中添加一些功能(包括全局函数和成员函数),然后调用它。

您可以创建模块,让用户能够动态地选择或设置您程序功能的序列。我在 Main 中放置了几个可能的简单变体。当然,在较大的程序中,所有 AddActionToSequencee 调用都必须包含在 try 块中,以捕获 std::bad_alloc

在本文的这部分(文件 DynamicActionSequence.cpp)的上下文中,我假设每个被调用的函数对象都完成了它的工作,而没有接收或返回任何参数。如果您想要一个更复杂的版本,可以处理参数,从被调用的函数对象返回,那么让我们看看下一个程序——基于之前的版本。 

版本 2 - 处理返回值

首先,让我们看看为了使这项功能可行而提出的必须解决的问题。函数对象返回的类型可能不同,因此存储返回值的最佳方法是使用模板存储。潜在地,ActionHolder 可以是存储值的宿主,但这些值可能在 ActionHolder 外部需要。因此,主要问题是

  1. 如何存储 void
  2. DynamicActionSequenceHolder 存储指向基接口的指针——它如何通过同一个虚函数返回不同的返回类型?
  3. 返回值处理器可能被传递,也可能不被传递。(顺便说一句,对处理器的要求是——它必须具有相同的接口才能被通用调用——它必须是一个接收一个参数的函数对象。)

我决定可以使用模板特化来解决这个问题。但不是特化 ActionHolder,而是特化辅助结构 RetValueStorage。它负责解决前两个问题。如果我们想通过相同的方式——使用相同的函数返回不同的类型,它必须通过某种联合体来完成,并存储被推导的已存储类型的标识(就像 MFC 中的 CDBVariant 一样)。我认为模板特化存储结构 RetValueStorage 以处理这些值将比任何运行时类型标识都快得多。第三个问题通过另一个辅助类 HandlerStub 的特化来解决。

现在,让我们看看在这个版本中谁是谁

  • ActionInterface – 用于通过虚函数调用存储的函数对象,将子类——ActionHolder 对象——正确删除地存储在同一个容器中,并存储和返回指向 Base_Value_Storage 结构模板子类的同一个指针。
  • 结构 Base_Value_Storage – 用于正确删除子类和存储返回类型 ID 的接口。
  • 结构 RetValueStorage – 模板结构,存储任何类型(除了 void)的返回值,并设置和存储返回类型 ID(这是通过对任何必须存储的类型进行特化来实现的。结果——void 特化没有 void 成员;RetTypeSpecificator 宏用于快速声明它们)。
  • 枚举 RetType – 仅用于在处理不同存储类型时方便。
  • RetTypeSpecificator – 用于声明 RetValueStorage 的特化的宏。
  • ActionHolder – 存储操作、返回值以及该值的处理器。程序只有一次处理器调用迭代。函数对象返回值和处理器在其上被调用。
  • CallFunctor()ActionHolder 中的成员模板函数。它与 HandlerStub(或实际处理器)一起解决了以下问题:为了方便起见,用户可以选择性地在 ActionHolder 类中放置一个指向现有处理器的指针。因此,如果用户没有为返回值提供实际的处理器——在 ActionHolder 构造函数中,我们传递了一个动态创建的 HandlerStub 对象。每个 ActionHolder 对象都存在一个处理器,即使函数对象的返回类型是 void。处理器是一个模板成员,并且可以在其函数对象中接收除 void 以外的任何返回类型的参数。因此,默认的 HandlerStub 类被特化了。调用 m_Action(); 时也发生同样的问题。这个函数对象必须根据存储的操作返回的类型来调用。此调用可以有一个返回值,也可以没有(如果返回 void)。结果,在虚函数内部,我们调用特化的 CallFunctor<>() 模板成员函数。在其 CallFunctor<void>() 特化中,它可以简单地调用 m_Action()。在所有其他 CallFunctor 的实例中,相应的存储会接收来自 m_Action() 调用的返回值。
  • TypeDeductor – 辅助类,用于在将数据提取到 ExtractValue 函数(作为引用传递)时,检查存储类型和缓冲区类型之间的相似性。如果类型相同——重载的 operator == 返回 true。
  • ExtractValue – 全局模板函数,用于从 ActionHolder 对象中存储在 RetValueStorage 对象中的数据进行提取。
  • HandlerStub – 当用户不想使用任何处理器时传递的实际存根。
  • 它必须被特化,因为它必须使用带有参数的函数对象调用其函数对象,而该参数不能是 void 对象!因此,特化为 void 的实例根本没有函数对象。

  • DynamicActionSequenceHolder – 管理与特定操作序列进行所有工作的管理器。其 AddActionToSequence 成员函数有两个重载版本——一个用于接收实际传递的处理器,一个用于不带任何处理器传递的操作。这是为了方便用户。
  • DataClass, A, B, C, D, Func, IntHandler, DoubleHandler – 用于测试此功能的工具。

要为此模块使用任何必需的类型,您必须

  1. RetTypeSpecificator.hpp 中包含新存储类型的声明文件。
  2. RetType 枚举添加一个新类型(如果尚未添加)。
  3. 使用宏 RetTypeSpecificator 和新类型。
  4. ExtractValue 函数中为您的类型添加一个新的 Case
  5. 如果需要,为您的类型定义处理器。
  6. 将您的操作和处理器添加到 DynamicActionSequenceHolder 对象中。

此描述通过 //:ref_1 标记进行了加倍(在 Find in files 对话框中使用它)。我在我所有的项目中都使用了这个技巧。它在添加新功能或查找 bug 时确实节省了大量时间。

代码是在 VS 2005(8.0) 上编译的。

祝您好运!!

© . All rights reserved.