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






4.17/5 (5投票s)
一个模块,用于动态分配、存储和调用具有不同返回值处理器的函数对象:函数对象、函数、成员函数。
引言
我想向您提供一个(我认为)不错的解决方案,以解决一个特定问题。假设您必须让您的程序用户能够动态分配任务,让程序来执行。或者您想编写一个可编程的测试模块——例如,您希望能够以任何顺序定义执行任务的任何序列。当然,您可以编写几个具有相同接口的类:编写一个抽象基类,继承几个不同的子类,然后创建一个指向它们的对象的基类指针列表,以调用相同的虚拟接口函数。但您必须继承它们,这意味着——您必须实现很多可能的事情。这至少会耗费时间。让我们从另一个角度来看待这个问题。在现实生活中,如果您想修理或制作某件东西,您会逐一拿起所需的工具,然后做您想做的事情。为什么不创建一个像我们的手(当然还有大脑)一样的工具,它可以封装任何特定的现有工具并使用它呢?我提出了两个类,它们将封装任何功能,还有一个类可以存储这些封装的任何序列。现在来谈细节。(要使用本文的代码,您必须知道如何在计算机上安装 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
使用两种多态性
- 它继承自接口——因此,指向它的指针可以与其他可能的子类的指针存储在同一个容器中,以便调用相同的
DoAction();
- 它是一个模板类——因此它可以存储在任何函数对象中,并由
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
外部需要。因此,主要问题是
- 如何存储
void
? DynamicActionSequenceHolder
存储指向基接口的指针——它如何通过同一个虚函数返回不同的返回类型?- 返回值处理器可能被传递,也可能不被传递。(顺便说一句,对处理器的要求是——它必须具有相同的接口才能被通用调用——它必须是一个接收一个参数的函数对象。)
我决定可以使用模板特化来解决这个问题。但不是特化 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
– 当用户不想使用任何处理器时传递的实际存根。DynamicActionSequenceHolder
– 管理与特定操作序列进行所有工作的管理器。其AddActionToSequence
成员函数有两个重载版本——一个用于接收实际传递的处理器,一个用于不带任何处理器传递的操作。这是为了方便用户。DataClass
,A
,B
,C
,D
,Func
,IntHandler
,DoubleHandler
– 用于测试此功能的工具。
它必须被特化,因为它必须使用带有参数的函数对象调用其函数对象,而该参数不能是 void
对象!因此,特化为 void
的实例根本没有函数对象。
要为此模块使用任何必需的类型,您必须
- 在 RetTypeSpecificator.hpp 中包含新存储类型的声明文件。
- 向
RetType
枚举添加一个新类型(如果尚未添加)。 - 使用宏
RetTypeSpecificator
和新类型。 - 在
ExtractValue
函数中为您的类型添加一个新的Case
。 - 如果需要,为您的类型定义处理器。
- 将您的操作和处理器添加到
DynamicActionSequenceHolder
对象中。
此描述通过 //:ref_1
标记进行了加倍(在 Find in files 对话框中使用它)。我在我所有的项目中都使用了这个技巧。它在添加新功能或查找 bug 时确实节省了大量时间。
代码是在 VS 2005(8.0) 上编译的。
祝您好运!!