以下内容为NS3官方文档翻译。
1.5 Callbacks 回调
某些 ns-3 的新手对代码中广泛使用的编程习惯不熟悉:“ns-3 callback”。本章
提供回调的设计动机、使用方法以及他的实现细节。
1.5.1 动机 Callbacks Motivation
考虑有两个模拟模型 A 和 B,并希望使他们在模拟过程中互相传递信息。一种方
法是使得 A 和 B 彼此知道对方,这样他们就可以彼此调用对方的方法。
class A {
public:
void ReceiveInput ( // parameters );
...
}
(in another source file:)
class B {
public:
void ReceiveInput ( // parameters);
void DoSomething (void);
...private:
A* a_instance; // pointer to an A
}
void
B::DoSomething()
{
// Tell a_instance that something happened
a_instance->ReceiveInput ( // parameters);
...
}
这样当然可行,但有缺点:为了使 A 和 B 彼此知道对方,在编译期间引入了 A
与 B 的依赖(这使得在模拟器很难获得独立的编译单元),并且不是一般化的。
如果在后期的使用情景下,B 需要与另一个完全不同的对象 C 来交流,那么 B
的源代码就需要添加一个“c_instance”,等等。容易看出这是一种通信的蛮力机
制,他将在模型中导致无用的、笨拙的编码。
但这并不表示对象不应该彼此知道对方,尤其是彼此之间存在牢固的依赖时。但
如果他们之间的交流在编译期间限制较少的话,模型可以被制定的更加灵活。
这不是网络模拟研究的一个抽象问题,当研究者想要扩展或修改系统以便来做一
些不同的事情时(as they are apt to do in research),它是先前一些模拟器的一
个问题的源泉。例如,用户想要在 TCP 层和 IP 层之间加入一个 IPsec 安全协议
子层:
----------------- -----------------
| TCP | | TCP |
----------------- -----------------
| becomes-> |
----------------- ----------------- ------------------
| IP | | IPsec | | | TCP |
----------------- ----------------- ------------------
如果仿真器采用硬编码,那么IP总是与上面的传输协议通信,用户可能必须劈开系统来得到期望的互联。这很清晰表明不是一种可选的方式来设计一个通用的仿真器。
1.5.2 Callbacks Background
注意:熟悉回调编程的读者可以跳过这一节内容。
基本的机制是允许你处理上面的问题的众所周知的方法是使用callback。最终的目标是允许一段代码调用一个没有内部模块依赖的函数(或者在C++中的方法)。
最终的意思就是你需要间接的方式-你需要处理将函数作为变量的地址。这个变量被成为指针函数变量。函数和指针函数之间与对象和指针对象之间并没有什么不同。
在C语言中,指针函数的经典代码是一个指针函数返回整数(pointer-to-function-returning-integer PFI)的例子。对于一个带有一个整型参数的PFI,可以像下面那样声明:
int (*pfi)(int arg) = 0;
从该行代码获取的是一个被简单的命名为pfi的变量,并被初始化为值0.如果你想初始化这个指针一个有意义的值,你必须有一个匹配签名的函数。如下例子:
int MyFunction (int arg) {}
如果你有这样的目标函数,你可以初始化上面的变量指向你的函数,如下:
pfi = MyFunction;
然后,你可以使用更多提示形式的间接的调用MyFunction函数:
int result = (*pfi) (1234);
由于在你取消引用指针情况下,你可以取消关联函数指针,所以这是一种提示的信息。然而,通常情况下,人们利用如下事实的优势,那就是编译器知道什么正在进行,它将仅仅使用短的形式:
int result = pfi (1234);
注意到函数指针遵循值语义,因此你给它传递任何其他的值。通常情况下爱,当你使用一个异步的接口,你将传递像这样的实体到一个函数,这个函数将表现一种行为,并回调来让你知道异步操作已经完成。它通过遵循这种间接方式并执行提供的函数来进行回调。
在C++中,你具有对象的添加的复杂性。类比与上面提到的PFI,你有一个指向返回整数的成员函数的指针(PMI),代替了返回整数的指针函数的指针(PFI)。
间接提供的变量的声明看起来稍微有点不同:
int (MyClass::*pmi) (int arg) = 0;
这里声明了一个名字是pmi的变量,正如前面的例子中声明一个名字为pfi的变量。由于pmi将调用一个具体类的实例的方法,你必须在类中声明这个方法:
class MyClass {
public:
int MyMethod (int arg);
};
给定了类的声明,然后你可以像下面那样初始化这个变量:
pmi = &MyClass::MyMethod;
这样把实现的方法的代码的地址分配给了这个变量,完成了间接性。为了调用方法,代码需要一个this指针。反过来,这意味着必须有一个MyClass对象来引用这个指针。间接的调用方法的一个简单的例子(考虑virtual函数):
int (MyClass::*pmi) (int arg) = 0; // Declare a PMI
pmi = &MyClass::MyMethod; // Point at the implementation code
MyClass myClass; // Need an instance of the class
(myClass.*pmi) (1234); // Call the method with an object ptr
就像在C代码的例子,你可以在一个异步情况下使用这种方法来调用另一个模块,而这个模块将使用一个方法和一个对象指针来回调。直接了当的扩展的情况是,你可以考虑传递一个指针到一个对象和一个PMI变量。模块仅仅作如下代码来在一个期望的对象上执行回调:
(*objectPtr.*pmi) (1234);
有人可能在此会问,what’s the point?调用的模块将不得不理解具体的调用的对象的类型来合适的调用回调。为什么步接受这样的方式,传递正确的类型的对象指针,并在代码中采用object->Method(1234)来代码回调呢?上面的问题是显而易见的。我们所需要的是一种调用的函数与具体的调用类脱钩。这种需求引导出Functor的开发。
Functor是在1960s发明的称为closure(闭包)的东西而生长起来的。It is basically just a packaged-up function call, possibly with some state.
一个functor有两部分,一个具体的部分,一个通用的部分,通过继承相联系。调用的代码(执行callback的代码)将执行一个通用的functor的一个通用的重载的operator()方法来引起callback被调用。被调用的代码(想要被calledback的代码)将不得不提供一个特定的实现的operator()方法,表现出类具体的工作,这引起上面提到的紧密耦合的问题。
使用具体的functor和它的重载的被创建的operator()方法,被调用的代码给出具体的实现的代码到将要执行callback(主动调用的代码)的模块中。
主动调用的代码将使用通用的functor作为参数,因此在函数中一个含蓄的投射就是把具体的functor转化成一个通用的functor。这意味着主动调用的模块(calling module)仅仅需要理解通用的funcor类型。这就完全从主动调用的代码(calling code)中解耦了。
你所需要的创建一个具体的functor的信息就是一个对象指针和一个指针函数的地址。
所需要的东西的本质就是系统声明的functor的通用部分。
template <typename T>
class Functor
{
public:
virtual int operator() (T arg) = 0;
};
调用者定义了functor的具体部分,这仅仅需要实现具体的operator()方法:
template <typename T, typename ARG>
class SpecificFunctor : public Functor<ARG>
{
public:
SpecificFunctor(T* p, int (T::*_pmi)(ARG arg))
{
m_p = p;
m_pmi = _pmi;
}
virtual int operator() (ARG arg)
{
(*m_p.*m_pmi)(arg);
}
private:
int (T::*m_pmi)(ARG arg);
T* m_p;
};
这里是用法实例:
class A
{
public:
A (int a0) : a (a0) {}
int Hello (int b0)
{
std::cout << "Hello from A, a = " << a << " b0 = " << b0 << std::endl;
}
int a;
};
int main()
{
A a(10);
SpecificFunctor<A, int> sf(&a, &A::Hello);
sf(5);
}
注意:
前面的代码不是真实的ns-3的代码。这个简单的示例代码仅仅论证涉及的概念,并更好的帮助你理解系统。不要期望在ns-3目录树中任何地方找到这个代码。
注意到在上面的类中有两个变量。m_p变量是对象指针,m_pmi变量包含了要执行的函数的地址。
注意到当operator()方法被调用,它顺次调用对象指针所提供的使用C++ PMI语法的方法。
为了使用这个,你可以声明一个模式代码,并使用一个通用的functor作为参数:
void LibraryFunction (Functor functor);
放在模式中的代码将构建一个具体的functor,并把它传递到LibraryFunction:
MyClass myClass;
SpecificFunctor<MyClass, int> functor (&myclass, MyClass::MyMethod);
当LibraryFunction完成,它用传递给通用functor的值来调用operator()方法进而执行callback,在这个特定的例子中,提供了一个整形参数:
void
LibraryFunction (Functor functor)
{
// Execute the library function
functor(1234);
}
注意到LibraryFunction被完全从客户端的具体的类型中解耦出来。通过functor的多态性完成联系。
在ns-3中的callback API使用functor机制实现了面向对象的回调。这个基于C++模板的callback API,是类型安全的,它使用静态类型检查来强制调用者和被调用者之间使用合适的兼容的签名。因此它相比于传统的函数指针,是更加的类型安全,但是语法可能看起来有些难度。本节来介绍回调系统,以便使你能够在 ns-3 中轻松地使用他。
1.5.3 使用回调 API (Using the Callback API)
回调 API 很精简,他提供两类服务:
1.回调类型声明:用给定的签名声明回调的类型。
2.回调实例化:实例化由模板生成的转发回调,该回调能够将所有调用转发至另一
个 C++的类成员方法或 C++函数。
最好通过例子 samples/main-callback.cc 来理解:
通过静态函数使用回调 API (Using the Callback API with static functions)
考虑函数:
static double
CbOne (double a, double b)
{
std::cout << “invoke cbOne a=” << a << “, b=” << b << std::endl;
return a;
}
考虑以下主程序片段:
int main (int argc, char *argv[])
{
// return type: double
// first arg type: double
// second arg type: double
Callback<double, double, double> one;
}
这个类模板回调实现了 Functor Design Pattern。他被用来声明回调的类型。他
包含 1 个必须的参数
(指派给该回调的函数的返回类型)和最多 5 个可选的参数,
分别指定了参数的类型(如果你的函数包含的参数多于 5 个,可以通过扩展回调
的实现来处理)。
我们在上述内容中声明了一个叫做”one” 的回调,该回调将保留一个函数指针。
他保留的函数必须返回 double 型,并且必须支持两个 double 型参数。如果试图
传递一个函数,该函数的签名不与所声明的该回调匹配,那么编译将失败。
现在我们需要将这个回调实例与实际的目标函数(CbOne)联系起来。注意
CbOne 的函数签名类型与回调的类型是相同的,这一点很重要。我们可以将类
型符合的任意函数传递给该回调。让我们更严密地研究一下:
static double CbOne (double a, double b) {}
^ ^ ^
| | |
| | |
Callback< double, double, double> one;
只有在签名匹配的情况下才能将函数与回调进行绑定。模板的第一个参数是返回
值,其他参数是函数签名的参数的类型。
现在我们将回调”one” 与签名匹配的函数进行绑定:
// build callback instance which points to cbOne function
one = MakeCallback (&CbOne);
在后边的程序中如果需要用到回调,则使用方法如下:
// this is not a null callback
NS_ASSERT (!one.IsNull ());
// invoke cbOne function through callback instance
double retOne;retOne = one (10.0, 20.0);
检查函数 IsNull() 确保该回调不为 Null,即该回调的背后存在一个函数来调用。
函数 one() 返回的结果与直接调用函数 CbOne() 返回的结果是相同的。
通过成员函数使用回调 API (Using the Callback API with member functions)
通常情况下,你调用的是对象的公共成员函数,而不是静态函数。在这种情况下,
MakeCallback 函数就需要一个额外的参数,该参数告诉系统 该函数应该在哪个
对象上被调用。考虑如下来自 main-callback.cc 的例子:
class MyCb {
public:
int CbTwo (double a) {
std::cout << “invoke cbTwo a=” << a << std::endl;
return -5;
}
};
int main ()
{
...
// return type: int
// first arg type: double
Callback<int, double> two;
MyCb cb;
// build callback instance which points to MyCb::cbTwo
two = MakeCallback (&MyCb::CbTwo, &cb);
...
}
这里我们传递一个(裸)指针至函数 MakeCallback<> ,即当函数 two () 被
调用时,调用由 &cb 所指的对象上的函数 CbTwo。
另一种变形使用是 当对象 被 ns-3 的 smart pointers 引用时,MakeCallback
API 接受裸指针,所以我们需要调用函数 PeekPointer () 来获得该裸指针。则
上述的例子将类似于:
class MyCb : public Object {
public:
int CbTwo (double a) {
std::cout << “invoke cbTwo a=” << a << std::endl;
return -5;
}
};
int main ()
{
...
// return type: int
// first arg type: double
Callback<int, double> two;
Ptr<MyCb> cb = CreateObject<MyCb> ();
// build callback instance which points to MyCb::cbTwo
two = MakeCallback (&MyCb::CbTwo, PeekPointer (cb));
...
}
构建 Null 回调(Building Null Callbacks)
回调可以是空的,因此在使用他们之前进行检查是明智的。有一种构建空回调的
方法,将”0′′ 作为参数传递即可。MakeNullCallback<> 构建:
two = MakeNullCallback<int, double> ();
// invoking a null callback is just like
// invoking a null function pointer:
// it will crash at runtime.
//int retTwoNull = two (20.0);
NS_ASSERT (two.IsNull ());
1.5.4 Bound Callbacks
1.5.5 Traced Callbacks
Placeholder subsection
1.5.6 回调在 ns-3 中的位置(Callback locations in ns-3)
回调通常在 ns-3 中的什么地方被用到?ns-3? 这里是一些对于典型用户比较
容易可见的例子:
• Socket API
• Layer-2/Layer-3 API
• Tracing subsystem
• Routing
• Route Reply
1.5.7 Implementation details
本节为对实现感兴趣的 C++专家提供进阶解释,大多数用户可以略过。
代码编写最初基于这里描述的技术,它通常随着如下体系结构被重写: Modern
C++ Design: Generic Programming and Design Patterns Applied–
Alexandrescu, chapter 5, “Generalized Functors”。
代码使用:
默认的模板参数,这使得当实际的参数数量小于最大支持的参数数量时,用户不必指定空参数。
the pimpl idiom:类 Callback 被通过值来传递,并且类 Callback 将任务的关键指派给他的 pimpl pointer。
由 CallbackImpl FunctorCallbackImpl 派生的两个 pimpl 实现可以通过任意functor-type 来使用,MemPtrCallbackImpl 可以通过指向成员函数的指针来使用。
引用列表来实现回调的值语义。
代码与 Alexandrescu 的实现显著不同,即没有使用类型列表来指定和传递回调参数的类型。当然,也没有使用 copy-destruction 语义,并依赖于引用列表而不是 autoPtr 来保留指针。
网友评论