本章主要介绍NS-3的对象框架当中的必不可少的TypeId。要使用TypeId的类,只要继承ObjectBase,然后重写TypeId即可。TypeId提供了一种动态创建对象的机制,弥补了C++的不足。使用TypeId还可以判断对象所属的继承关系。此外,TypeId还为后来的属性框架(Attribute Framework)和追踪框架(Tracing Framework)提供了必要的支持。可以想象TypeId完成了Java当中Class类所完成的某些功能,然而其功能远远没有Class类强大,并且大部分操作都是靠编译时处理的,无法像Java当中一样动态卸载和加载类型。
1. TypeId简介
C++是一种静态语言,无法动态的创建类和对象,也无法像Java一样是用反射机制从配置文件读取字符串,再通过字符串创建对象。为了弥补这种缺陷,NS-3实现了一套机制,称为TypeId,不但可以动态从字符串创建对象,还可以判断对象的类型归属。除此之外,TypeId还是NS-3的属性框架(Attribute Framework)和追踪框架(Tracing Framework)的基础。没有TypeId的支持,NS-3的对象框架将无法运作。因此在学习其他对象框架的特性之前,必须先对NS-3的TypeId有一定的了解。
TypeId可以认为是NS-3提出的一种机制,但实际上,TypeId只是NS-3类库当中的一个类。TypeId一般被需要使用NS-3对象框架的其他类创建并使用。这些类需要在一个名为GetTypeId()的方法中创建TypeId的实例,这个TypeId的实例中存储了创建它的类的一些基本信息,包括:
- 名称空间是什么
- 类的名字是什么
- 父类是谁
- 构造函数是什么(以及是否有构造函数)
- 有哪些属性(属性框架)
- 有哪些值的改变需要追踪(追踪系统)
- 有哪些事件的发生需要追踪(追踪系统)
- ……
TypeId所记录的类的名字是用字符串表示的。此后通过这个字符串就可以创建出对象的类。这在很大程度上非常类似于Java的反射机制。 然而,TypeId本身是没有任何作用的。它必须在一个类的内部定义,而这个类一般都继承自ObjectBase。因为NS-3配合ObjectBase类提供了一些非常有用的工具类。
2. ObjectBase
ObjectBase是整个NS-3对象框架的基础。为NS-3的其他对象功能提供必要的支持。首先来看看ObjectBase的代码结构:
1 | class ObjectBase |
其中的公有方法可以大概分成三大功能模块:
- TypeId支持
- GetTypeId():通过类获得当前类的TypeId
- GetInstanceTypeId():通过类的实例获得其TypeId
- 属性框架支持
- SetAttribute():为对象设置一个属性
- SetAttributeFailSafe():为对象设置一个属性,出现错误的时候不会停止运行
- GetAttribute():获取对象的某个属性
- GetAttributeFailSafe():获取对象的某个属性,出现错误的时候不会停止程序的运行
- 追踪框架支持
- TraceConnect():链接对象的某个追踪源
- TraceConnectWithoutContext():连接对象的某个追踪源,但是不携带上下文信息
- TraceDisconnect():断开对象的某个追踪源,不再继续追踪属性的变化和事件的发生
- TraceDisconnectWithoutContext():断开不带上下文的某个追踪源
属性系统和追踪系统会在后面章节进行介绍,本章我们先介绍TypeId。
GetTypeId()是一个静态方法,因此,可以直接通过类来调用。例如ObjectBase::GetTypeId()。而GetInstanceTypeId()是一个实例方法,并且是一个虚方法,因此调用的时候将根据实际的类型调用其子类的重写方法。除此之外,在ObjectBase当中,GetInstanceTypeId()是一个纯虚方法,这就意味着ObjectBase是一个抽象类,无法被实例化。继承ObjectBase的具体类,必须实现GetInstanceTypeId()方法并返回一个TypeId值。如果没有特殊的需求的话,GetInstanceTypeId()所返回的TypeId值,应该和GetTypeId()值一致,否则就会出现实例的TypeId和类型的TypeId不一致的情况,因此一般都是直接在GetInstanceTypeId()当中调用GetTypeId()静态方法。
3. TypeId
既然GetTypeId()和GetInstanceTypeId()都返回TypeId类型,那么我们有必要详细了解以下TypeId这个类。我们从TypeId的源代码中选择了部分公有方法,去除了和属性框架以及追踪框架相关的部分方法。源代码如下:
1 | …… |
这里,我把TypeId的核心方法分成了4类:
- 构造函数和操作符重载:这类方法定义了我们如何创建和比较一个TypeId
- 查找TypeId和相关信息:这类方法都是静态方法,定义了我们如何能够获得一个已经存在的TypeId,以及获得全局TypeId的相关信息
- 通过TypeId设置和获取类的信息:这类方法定义了我们如何通过TypeId的实例去设置一些和其所描述的类相关的信息
- 通过TypeId解析类相关的继承层次关系:这类方法使得我们可以了解该TypeId的实例所描述的类的继承层次关系
4. 对象框架
4.1. 对象模板代码
要创建一个支持NS-3对象框架的类,需要让对象至少继承自ObjectBase,这个类至少应该重写GetTypeId()和GetInstanceTypeId()两个方法。例如,如果我们需要创建一个名字为MyObject的对象,那么我们需要至少实现如下代码:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
以上就是一个基本的类在NS-3中的实现。其中有几点需要注意:
- 类最好写在ns3名称空间中,当然要用其他名称空间也是可以的,但是写在ns3空间中,无论使用继承还是调用其他类都会比较方便
- 使用NS-3的对象框架,类至少要继承自ObjectBase
- 提供GetTypeId()方法的实现,其中创建一个静态的TypeId对象,然后需要设置其属性:
- Name:作为构造函数的参数传入,表示类的名字,包含名称空间。注意:无论是类名还是名称空间的名字,都无需和实际对象一致,只要提供唯一的字符串即可,但一般为了避免产生歧义和误导用户,我们一般选择让类名的字符串和实际类的名字和所处的名称空间一致
- Parent:通过SetParent()方法设置类是从哪个类继承的,传入父类的TypeId值,这方便我们仅仅通过TypeId就可获得类的继承关系,方便以后程序逻辑的书写
- GroupName:设置一个类所属的组别,可以不设置,无实际意义,仅仅是为了区分不同的对象类别
- Constructor:为类型添加一个构造方法,通过AddConstructor()方法进行设置,无参数,使用模板类型进行标示。注意:如果类型是一个抽象类,则无法添加构造方法
- GetTypeId()必须是静态方法,此外,其中必须创建并返回一个TypeId对象,
- 可以使用C++的static关键字修饰该变量,以避免每次调用GetTypeId()方法时重复创建该对象
- 推荐使用下面这种更加安全的创建方法:这种方法在创建tid的时候就设置和添加了TypeId的各种属性。否则可能出现在反复调用GetTypeId()方法时,重复添加构造函数的错误。
1
2
3
4
5
6
7
8
9
10
11……
TypeId
MyObject::GetTypeId ()
{
static TypeId tid = TypeId("ns3::MyObject")
.SetParent(ObjectBase::GetTypeId())
.SetGroupName("MyExample")
.AddConstructor<MyObject>();
return tid;
}
……
- 实现GetInstanceTypeId()方法,在没有特殊情况的时候,此方法一般返回类的TypeId,该方法必须有const后缀修饰
- 最佳实践推荐使用NS_LOG_COMPONENT_DEFINE宏定义一个日志组件,组件的名字推荐和类名一致
- 必须调用NS_OBJECT_ENSURE_REGISTERED宏来在NS-3对象框架中注册新创建的类,否则无法实现NS-3对象框架的特性
- 如果类分为头文件和源文件,则上面两个宏需要在源文件中调用,并且最好是写在名称空间中
实现了以上几点之后,一个类就加入了NS-3的对象框架,具有了NS-3类最基本的特性。
4.2. 创建对象
加入了NS-3对象框架之后,就可以通过多种方法来创建对象。最常见的方法就是通过类的名字来创建,如下的代码所示:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
代码中,使用类的名字的字符串查找到了类所对应的TypeId,然后使用TypeId的方法GetConstructor()获取了该类的构造函数的回调(callback,类似于函数指针,将在后面章节详细介绍)。随后,我们调用了该构造函数,并创建了类的一个实例。由于回调函数只能返回基类类型,但我们明确创建的是子类类型,因此这里使用C++的dynmaic_cast方法将指针转换成子类类型。因为使用子类的指针才能正确调用子类的方法。需要注意的是,由于程序中是直接创建了对象,并获取了对象的指针,而未使用智能指针,因此,必须在适当的时候自己删除所创建的对象。当然C++的最佳实践还指出,我们删除了对象之后,还应该将指向该对象的指针赋值为空,这样在以后的程序中才能正确通过判断来确定指针是否为空。
运行程序得到如下输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,对象能够通过TypeId被成功创建、使用和销毁。
4.3. 反射机制
由于现在可以从字符串动态地创建对象,因此,我们甚至可以模仿Java中Class.forName()类似的反射机制,从配置文件中读取类的配置信息,动态创建出具体对象。看下面的例子:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
我们创建了两个对象MyObject2和MyObject3都继承自MyObject,且分别重写了MyObject的虚方法MyMethod()。然后我们在ns-3的主目录中创建一个名为MyObjectConfig.ini的文本文件,内容如下:
1 | ns3::MyObject3 |
然后我们运行该程序得到如下输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
换句话说,我们通过从文件读取出来的字符串创建了MyObject3对象,并成功调用了其方法。如果我们将文件中的字符串改为:
1 | ns3::MyObject2 |
那么程序运行结果将变为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
值得注意的是,此处,并没有重新编译程序。因为仅仅有配置的文件的改变,而没有程序的改变,但是我们看到的实际结果却是创建出来的对象变成了MyObject2,并且成功调用了其方法。这以前是必须使用Java的反射机制才能实现的功能,现在使用NS-3的对象框架也可以实现。通过这种方法,可以让我们在不用重新编译程序的情况下,动态地决定创建的对象实例究竟是属于哪个类型。
4.4. 和SimpleRefCount结合
上面的代码中使用的依然是普通指针,如果需要使用智能指针来引用对象,最好的方式就是像上文一样让对象继承SimpleRefCount。如果参考上一章中提到的SimpleRefCount的代码可以发现,SimpleRefCount可以通过模板的形式指定一个父类(在上一章的例子当中我们一直未指定该父类,因此父类默认值为空):
1 | template <typename T, typename PARENT = empty, typename DELETER = DefaultDeleter<T> > |
实际上,将SimpleRefCount的父类指定为ObjectBase的话,便可同时拥有SimpleRefCount支持智能指针的特点和ObjectBase支持TypeId的特点。
例如上面的MyObject类,即可改为:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
和之前的MyObject类相比,现在的MyObject类现在继承了SimpleRefCount,因此具备了使用智能指针的能力。同时,我们又通过模板,让SimpleRefCount继承了ObjectBase,因此,类又可以使用TypeId来创建对象,具有了反射的能力。因此在创建对象的时候,可以直接将普通指针转换成智能指针:
1 | …… |
当然,如果每次都这么创建对象,那么语法方面是相当复杂的,并且很容易出错。得益于C++强大(?)的模板支持,我们可以用模板写一个通用方法:
1 | template<typename T> |
可以看出,这个方法和上一个代码片段执行的功能是一样的,只是类型变成了更通用的T模板。以后可以通过直接调用这个方法来方便地创建任何对象的智能指针(当然,前提是类必须支持智能指针):
1 | Ptr<MyObject> obj = CreatePtrObject<MyObject>(); |
这种写法比上面的写法要简单很多,并且不容易出错。对象被成功创建以后,在程序中就无需再显式地销毁对象,而智能指针会管理对象的引用,在引用减少到零的时候,对象自动会被销毁。运行程序后,得到如下输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可以看出,对象被正常创建,并且可以通过智能指针正确地调用,而指针销毁的时候,对象被同步销毁。
4.5. 使用TypeId判断对象的类型
通过TypeId可以判断真实的对象和类的从属关系,然后通过智能指针的动态转换转换智能指针的类型。例如在如下的代码中:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
MyObject2和MyObject3两个类继承了MyObject类,前两者分别有自己特殊的方法MyMethod2()和MyMethod3()。程序中具有一个函数TestType(),该函数接受一个MyObject对象的智能指针,然后判断这个指针所指向的对象到底属于哪个类型,然后才确定调用具体的哪个方法。在实现这个功能的过程中,需要注意以下几点:
- 必须重写每个类的GetInstanceTypeId()方法(虽然代码都是一样的)
- 在上一条的基础上,可以使用实例的TypeId的值和类的TypeId的值来对比判断具体的类型归属
- 在上一条的基础上,可以使用智能指针提供的动态转换方法DynamicCast<T, U>()来进行智能指针的类型转换,例如:
1 | …… |
上面的代码中将MyObject类型的智能指针转换成MyObject3类型的智能指针。当然,前提是这两种指针是能够互相转换成功的,一般这种情况多出现在要将父类的指针转换成子类的指针的时候。
在上面的例子中,一旦指针转换成功后,即可调用不同子类的独有方法。例如MyObject2有自己的独有方法MyMethod2(),而MyObject3有自己的独有方法MyObject3()。
运行上面的程序后得到的运行结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
从运行结果可以看出,TestType方法中,我们可以通过TypeId区分到底是传入的是MyObject2对象还是MyObject3对象,并正确调用其独有方法。
4.6. 使用TypeId判断类的继承关系
TypeId除了可以判断对象和类的归属之外,还可以判断类之间的继承关系,或者获取父子类的信息。例如,我们在上面程序的基础上进行修改,再添加两个类MyObject4和MyObject5分别继承自MyObject2和MyObject3。我们修改程序的TestType方法,只接受MyObject2及其子类,才能调用方法,而MyObject3及其子类,将提示错误。
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
从TestType()函数中可以看出,如何判断类的继承关系。其中if语句有两个条件,第一个条件表示传入的对象类型和MyObject2的类型一致;第二个条件表示传入的对象类型是MyObject2类的子类。当满足这两个条件时,我们认为传入的对象类型可接受。
该程序运行的结果如下:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
当传入的类型不满足条件的时候,我们认为传入的对象不可接受,然后输出错误信息,同时,程序通过TypeId的GetParent()方法可以轻易地获得该类的父类信息。这里,我们简单地获取了其父类的名字。我们将创建的对象的类型改为MyObject5,然后再次运行程序,可以得到如下输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.27/ns-3.27/build' |
可见,从TypeId可以正确地获取父类的信息。