NS3做为一个网络仿真库,出于性能的考量选择了C++。在写仿真程序时,不可避免的要对各类实体进行建模,天然C++中的class成了惟一可选的方案。不加任何技巧的class的确能够知足对某些实体的建模,但是在仿真软件的编写中须要有足够的动态性,好比有这样一些需求:前端
这些都不是过度的需求,若是真的写过仿真程序的话确定会很是渴求使用的软件可以提供实现这些需求的方法。要本身干巴巴的实现这些需求也不是不能够,好比能够提供一些查询接口来实现1;对于2的话,Qt的signal/slot或许能够实现。说到Qt了,其实QObject拥有了超越普通C++ class的能力,也都能知足上面指出的这些需求,可是其解决方案彷佛有点重。c++
幸亏,NS3经过TypeId能够很好的解决上面提出的各种需求。数据库
TypeId
是什么class TypdId { uint16_t m_tid; }
这就是TypdId,就是这么简单。彷佛有些难以想象,但TypdId
自己只是一个16bit的整型,以前提到的全部的复杂功能都是藏在TypdId
背后的IidManager
完成的,这个m_tid
只是做为一个索引而存在。编程
TypdId提供了很是多的方法,好比说增长一个属性(AddAttribute
),增长一个TraceSource(AddTraceSource
),这些方法只是直接了当的将所需信息搜集起来转发给IidManager
。能够看个例子:设计模式
TypeId TypeId::AddAttribute (std::string name, std::string help, uint32_t flags, const AttributeValue &initialValue, Ptr<const AttributeAccessor> accessor, Ptr<const AttributeChecker> checker, SupportLevel supportLevel, const std::string &supportMsg) { NS_LOG_FUNCTION (this << name << help << flags << &initialValue << accessor << checker << supportLevel << supportMsg); IidManager::Get ()->AddAttribute (m_tid, name, help, flags, initialValue.Copy (), accessor, checker, supportLevel, supportMsg); return *this; }
基本上全部的TypdId
的方法都是这个样子。因此解决问题的核心实际上是IidManager
。IidManager
可认为是一个类型数据库,保存了与TypdId
想关联的Attribute与TraceSource。具体的内部实现就太细节了,做为使用方是不须要也不该该去关注的。网络
TypdId
正如在Qt中同样,想要使本身写的一个类拥有强大的能力,须要本身动手在类的声明中添加Q_OBJECT。在NS3设计的TypeId
系统中,这个步骤是要给本身的class添加一个静态方法static TypeId GetTypeId (void)
,而后在这个函数里返回一个TypeId
。在这个过程,能够尽情的使用TypeId
提供的各类方法来给本类加属性和TraceSource,惟一的限制就是这个返回的TypeId
应该是处于GetTypeId
里的静态变量,这是为了保证全局的惟一性。固然了,写C++的限制多了去了,这个规则应该纳入这个NS3库的使用方法吧,不太值得吐槽。框架
TypeId
对于咱们平时写程序的最大的帮助在于,它能够给本身的类添加Attribute和TraceSource。函数
Attribute能够表明该实体的一些属性,好比说一台PC的IP地址、一只猫的体重等。你可能会想,这个不就是一个Get函数的事儿么,值得专门搞这么一套复杂的系统么。其实还真值得:你会去写一个127.0.0.1仍是2130706433?在NS3里,能够直接写127.0.0.1,这也得归功与这个Attribute系统。性能
TraceSource可类比Qt的Signal,在须要的时候调用这个Functor(想不到更好的名称了,不过写C++的应该都知道这个东西),连到这个TraceSource的其余函数(所谓的Slot)就会被自动调用。好处自没必要多说,要知道Qt能获得普遍的承认,Signal/Slot功不可没,NS3里的TraceSource系统就是Signal/Slot的翻版。ui
还有一个使用限制就是,须要经过一个宏来告知系统我一个TypeId
要注册:NS_OBJECT_ENSURE_REGISTERED
。这个宏其实声明了一个静态类并同时声明了一个本文件内的静态实例。在这个静态类的构造函数中调用了咱们定义的类的GetTypeId
,这就实现了自定义TypeId
的注册。
终于到重头戏了。其实这两部分的代码都切实的体现了C++罪恶的地方:模板与宏。一个新手要看懂这些代码要补充太多的东西了。说来惭愧,其实我自身也是一个新手,从开始接触这个库到如今能初步搞明白这样一个系统(真的只是大体初步明白),已通过去了3年多。这个系统的实现是模板里套宏、宏里套模板,看的时候须要时刻注意这段代码是宏生成的代码仍是用了模板。
前面提到了咱们能够用127.0.0.1来表明ip的那32个bit,这就是Attribute系统的神奇所在。在这个例子里,127.0.0.1实际上是一个字符串,Attribute系统能够把字符串转化为任何一种类型(能够是自定义类型)。
就单纯的以这个地址类型来解释好了。咱们的程序中须要使用IP地址,其最合适的存储方式实际上是一个int,但IP地址最适合人类的表述方式实际上是点分表示,咱们天然也想在使用的时候用这种方式。那这个应该怎么作?
首先先无论人怎么读写的问题,先考虑程序中的这个属性的使用方式。做为一个Ipv4的值,确定有一些相关联的方法,好比说是否为广播地址、是否为多播地址、是否为本地地址之类类的。这些能够以用成员函数的方式实现,既然这样,那就尽情的实现吧!不须要考虑怎么集成到Attribute系统中去。同理,这个类里面有什么字段,想要什么字段就尽情的加。想必你也看出来了,咱们在实现一个Attribute的时候,其实根本不须要考虑什么集成的问题。
可以用ns3的方式来给一个对象设置属性的这个能力依赖与3个基本的组件
AttributeValue
AttributeAccessor
AttributeChecker
首先看看什么是ns3的方式为一个对象设置属性,看一下官方manual里的例子
Ptr<ConfigExample> a2_obj = CreateObject<ConfigExample> (); a2_obj->SetAttribute ("TestInt16", IntegerValue (-3)); IntegerValue iv; a2_obj->GetAttribute ("TestInt16", iv);
第一行建立了新的对象ConfigExample
,并存在指针a2_obj
里。第二行就是所谓的ns3的方式设置属性,依赖于一个方法SetAttriute
。这个方法属于ObjectBase
,全部能用Ptr
指向的对象都是objectBase
的子类。因此说,在调用SetAttribute
时,除去C++的语法糖,这句话完整的形式是这样的:
SetAttribute (a2_obj, "TestInt16", IntegerValue (-3));
好了,咱们跳进去看看实现
void ObjectBase::SetAttribute (std::string name, const AttributeValue &value) { NS_LOG_FUNCTION (this << name << &value); struct TypeId::AttributeInformation info; TypeId tid = GetInstanceTypeId (); if (!tid.LookupAttributeByName (name, &info)) { NS_FATAL_ERROR ("Attribute name="<<name<<" does not exist for this object: tid="<<tid.GetName ()); } if (!(info.flags & TypeId::ATTR_SET) || !info.accessor->HasSetter ()) { NS_FATAL_ERROR ("Attribute name="<<name<<" is not settable for this object: tid="<<tid.GetName ()); } if (!DoSet (info.accessor, info.checker, value)) { NS_FATAL_ERROR ("Attribute name="<<name<<" could not be set for this object: tid="<<tid.GetName ()); } }
这个方法是对象本身身上的方法,因此要记住this
这时候指向的是谁:这里就是a2_obj
。这个方法也很直白
GetInstanceTypdId()
拿到真正的、ConfigExample
的TypeId
AttributeInformation
,就有了accessor
、checker
,还有做为参数传进来的值value
。DoSet
作了实际的设置工做再看看DoSet
:
bool ObjectBase::DoSet (Ptr<const AttributeAccessor> accessor, Ptr<const AttributeChecker> checker, const AttributeValue &value) { NS_LOG_FUNCTION (this << accessor << checker << &value); Ptr<AttributeValue> v = checker->CreateValidValue (value); if (v == 0) { return false; } bool ok = accessor->Set (this, *v); return ok; }
检查什么的就不说了,最让人关心的是这个方法accessor->Set (this, *v)
。这个方法是怎么定义的,是哪里来的?这下欢迎进入模板与宏的世界。
AttributeAccessor
答案是这个这个方法是属于accessor
的,而accessor
的定义是在注册TypeId的时候生成的。RTFSC:
class ConfigExample : public Object { public: static TypeId GetTypeId (void) { static TypeId tid = TypeId ("ns3::A") .SetParent<Object> () .AddAttribute ("TestInt16", "help text", IntegerValue (-2), MakeIntegerAccessor (&A::m_int16), MakeIntegerChecker<int16_t> ()) ; return tid; } int16_t m_int16; }; NS_OBJECT_ENSURE_REGISTERED (ConfigExample);
看到那句MakeIntegerAccessor (&A::m_int16)
了么?搞懂了这个,其实就能搞懂ns3的套路了,再看其余的机制也就顺风顺水了。咱们慢慢来,一步一步来,保证每一步都善始善终,不会出现跳跃的现象。这个过程稍微有点冗长,能够去拿包零食边吃边看了。
MakeIntegerAccessor
是可调用的一个“东西”。回想一下C++可调用的东西有哪些?1. 函数,2. Functor,就是实现了自定义operator()的一个class的实例。3.实例化一个类型看起来也像是函数调用。我用的Eclipse,f3跳转到定义,等我过去的时候傻眼了:
ATTRIBUTE_ACCESSOR_DEFINE (Integer);
好家伙,看来要展开这个看看了,ctrl+=让它现形:
template <typename T1> \ Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1) \ { \ return MakeAccessorHelper<IntegerValue> (a1); \ } \ template <typename T1, typename T2> \ Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1, T2 a2) \ { \ return MakeAccessorHelper<IntegerValue> (a1, a2); \ }
这展开了2个函数,到这时能够肯定,MakeIntegerAccessor
是一个函数,并且咱们调用的是只有一个入参的那个函数,这个函数返回了一个AttributeAccessor
的智能指针。具体的展开过程就不细讲了,也没有讲的必要,看看ATTRIBUTE_ACCESSOR_DEFINE
的定义就明白了。如今须要关心的是咱们如今调用的函数里有个T1
,要搞明白这个T1
的类型是什么。
从新回头看看MakeIntegerAccessor (&A::m_int16)
,这里的T1
就是&A::m_int16
的类型。先就此打住,这个结论先记下来。咱们继续追下去,这下应该看真正的实现MakeAccessorHelper<IntegerValue> (a1)
:
// 第一种实现 template <typename V, typename T, typename U> inline Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (U T::*memberVariable) // 第二种实现 template <typename V, typename T, typename U> inline Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (U (T::*getter)(void) const) // 第三种实现 template <typename V, typename T, typename U> inline Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (void (T::*setter)(U))
结果就是匹配到了第一种实现。
其实我曾经不少次追到了这里,却没看懂这里的类型究竟是什么意思。也不知道何时突然就明白了。A::m_int16
对应于U T::*
,是个正常人第一眼看上去绝对不会明白这究竟是怎么联系在一块儿的,我也是正常人,因此我如今也不明白这种怪异的语法究竟是谁第一次使用的。T
对应于A
,那么U
应该是对应于m_int16
。这个类型能表明一个类里的一个成员变量的类型,T
代表了它是一个类的成员变量,U
代表了这个变量的类型是uint16_t
,如今就只能这么死记了,要想真正搞明白我以为应该去翻一下编译器里前端究竟是怎么解析这个鬼畜般的语法的,先就这么囫囵吞枣吧!对于另外的两个反而更好懂一点,那个类型和平时用的函数指针类型声明挺像的,反而不用多说。一个是getter
,说明这个attribute只提供了获取的接口;一个是setter,说明这个attribute
只能设置不能获取。固然了,这是站在ns3的使用方式上说的,直接强行用c++的方式赋值不在咱们的讨论范围以内。
这3个函数都返回了一个指向AttributeAccessor
的指针。如今来看看实现吧!
Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (U T::*memberVariable) { /* AttributeAcessor implementation for a class member variable. */ class MemberVariable : public AccessorHelper<T,V> { public: /* * Construct from a class data member address. * \param [in] memberVariable The class data member address. */ MemberVariable (U T::*memberVariable) : AccessorHelper<T,V> (), m_memberVariable (memberVariable) {} private: virtual bool DoSet (T *object, const V *v) const { typename AccessorTrait<U>::Result tmp; bool ok = v->GetAccessor (tmp); if (!ok) { return false; } (object->*m_memberVariable) = tmp; return true; } virtual bool DoGet (const T *object, V *v) const { v->Set (object->*m_memberVariable); return true; } virtual bool HasGetter (void) const { return true; } virtual bool HasSetter (void) const { return true; } U T::*m_memberVariable; // Address of the class data member. }; return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false); }
照样很鬼畜。这是在函数里定义了一个类,而且返回了指向这个类的只能指针。这个类继承自AccessorHelper
,而AccessorHelper
又继承自AttributeAccessor
。因此将其做为AttributeAccessor
的子类返回也说得过去。
至于为何要继承这么多?我如今的理解是这样的
AttributeAccessor
只是一个纯虚接口,它只定义了做为Accessor应当具备的接口。在Java里的话,估计这就是个Interface。AccessorHelper
提供了Set
和Get
的默认实现,把一些可变的部分留给了它的子类来实现,这些可变的部分是DoSet
和DoGet
。因此在MemberVariable
要实现DoSet
和DoGet
。这应该是某种设计模式,看看那本书就能找到了。到如今为止,咱们知道能够造出来一个AttributeAccessor
,并把指向这个AttributeAccessor
的指针存在了咱们的IidManager
的数据库中。之后想要拿出来这个AttributeAccessor
,就要手拿TypeId
去找IidManager
去要,并且要到的也是一个指针,这个指针指向了在return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false);
这句话里的那个new出来的地址。
总结一下,一个类型的AttributeAccessor
只有一个,就是那个new出来的地方。程序其余地方都是拿指针去访问的。在那块地址存的东西只有两样(只考虑咱们如今这个membervariable类型的accessor)
U T::*m_memberVariable
的值,这个值表明了这个变量在拥有TypeId
那个类里的偏移量AttributeAccessor
的实例是有虚表指针的,这个虚表里就是真正的、对应类型的函数实现。回头看看那个DoSet
,里面那个accessor究竟是什么应该已经清楚了。那个个accessor的Set
方法在哪儿定义的?答案是AccessorHelper
。我直接把结论公开了,可是你如今应该停下来去看看具体的实现。AttributeAccessor
->AccessorHelper
->DoMakeAccessorHelperOne()里的MemberVariable
这是一条继承链,到了最下一层的时候全部的方法都已经定义,只是在不一样的层次提供了不一样的实现。
假设你已经搞明白Accessor
的继承链条了,也明白这个Accessor
到底支持什么操做,咱们就进入了真正执行Set
的地方:
// DoMakeAccessorHelperOne()里的MemberVariable的方法 virtual bool DoSet (T *object, const V *v) const { typename AccessorTrait<U>::Result tmp; bool ok = v->GetAccessor (tmp); if (!ok) { return false; } (object->*m_memberVariable) = tmp; return true; }
这里的V *v
就是myObj->SetAttribute ("MyIntAttribute", IntegerValue(3));
里的IntegerValue(3)
。要是没看懂,就去翻代码。这个结论是必需要搞懂的,否则就没有进行下去的必要了。
其实Accessor
的世界已经探索的差很少了,为了真正搞明白这个函数作了什么,咱们先转向看看AttributeValue
。
AttributeValue
NS3的套路是什么?用宏和模板作代码生成。这个套路在AttributeValue
里也是同样的。自定义了一个AttributeValue
须要写一个宏,这个宏帮助咱们作了大部分的事情。拿那个IntegerValue
说事儿:
ATTRIBUTE_VALUE_DEFINE_WITH_NAME (uint64_t, Uinteger); // 在头文件里写这个宏,可以展开为以下的定义 class UintegerValue : public AttributeValue \ { \ public: \ UintegerValue (); \ UintegerValue (const uint64_t &value); \ void Set (const uint64_t &value); \ uint64_t Get (void) const; \ template <typename T> \ bool GetAccessor (T &value) const { \ value = T (m_value); \ return true; \ } \ virtual Ptr<AttributeValue> Copy (void) const; \ virtual std::string \ SerializeToString (Ptr<const AttributeChecker> checker) const; \ virtual bool \ DeserializeFromString (std::string value, \ Ptr<const AttributeChecker> checker); \ private: \ uint64_t m_value; \ } // 上述定义的实现仰赖于对于cc文件里的实现,也是用宏 ATTRIBUTE_VALUE_IMPLEMENT_WITH_NAME (uint64_t,Uinteger); // 展开后是这样的 UintegerValue::UintegerValue () \ : m_value () {} \ UintegerValue::UintegerValue (const uint64_t &value) \ : m_value (value) {} \ void UintegerValue::Set (const uint64_t &v) { \ m_value = v; \ } \ uint64_t UintegerValue::Get (void) const { \ return m_value; \ } \ Ptr<AttributeValue> \ UintegerValue::Copy (void) const { \ return ns3::Create<UintegerValue> (*this); \ } \ std::string UintegerValue::SerializeToString \ (Ptr<const AttributeChecker> checker) const { \ std::ostringstream oss; \ oss << m_value; \ return oss.str (); \ } \ bool UintegerValue::DeserializeFromString \ (std::string value, Ptr<const AttributeChecker> checker) { \ std::istringstream iss; \ iss.str (value); \ iss >> m_value; \ do { \ if (!(iss.eof ())) \ { \ std::cerr << "aborted. cond=\"" << "!(iss.eof ())" << "\", "; \ do \ { \ std::cerr << "msg=\"" << "Attribute value " << "\"" << value << "\"" \ << " is not properly formatted" << "\", "; \ do \ { \ std::cerr << "file=" << "D:\\Code\\ns-allinone-3.28\\ns-3.28\\src\\core\\model\\uinteger.cc" << ", line=" << \ 35 << std::endl; \ ::ns3::FatalImpl::FlushStreams (); \ if (true) std::terminate (); \ } \ while (false); \ } \ while (false); \ } \ } while (false); \ return !iss.bad () && !iss.fail (); \ }
具体的展开过程感兴趣的能够去追一下,要是没有IDE的帮助,要展开一个这么复杂的宏也仍是须要一点时间的。结合以前accessor的内容(v->GetAccessor
),这个函数就定义在了这里(头文件里做为模板类成员函数实现了)。
值得一看的却是那两个SerializeToString
和DeserializerFromString
。这两个函数完成了字符串到实际定义类型的转换,里面用到了<<
的重载,因此这也是为何在自定义属性的时候要去实现全局operator<<
的缘由了。经过这两个函数,咱们就能够用一个字面意义上的127.0.0.1去设置一个IP,而非2130706433。其实这个系统在解析string的时候出了一点小bug被我发现了,也算是对开源的一点点小贡献吧!(https://www.nsnam.org/bugzill... 这个bug在ns3.25里提出来,以后应该是修好了。
还剩一个AttributeChecker
,但这个好像不影响对系统的理解,就不去看啦!想必搞明白套路以后,要看懂也不是什么难事儿啦!
TraceSource
Callback
杂谈提及TraceSource,那么Callback
就是绕不过去的坎。能够做为一个TraceSource
的属性关联一个TracedCallback
类型,用于通知自身值的改变。TracedCallback
只是一个Callback
的容器,里面有一个std::list<Callback>
用于存放链接到该TraceSource
的函数。一个TraceSource
能够链接屡次,每次它被Connect
一次,就会往这个list里填一个元素。固然,这个元素就是一个Callback
。
Callback
类自己只是提供了建立的接口于调用接口。调用接口就是对各个operator()
的重载,最多有9个参数,也所以有9个operator()
。这种状况在c++11
以后应该会有更好的写法,只是我并不知道怎么写罢了。Callback
继承自CallbackBase
,这里存放了真正的指向实现(CallbackImplBase
)的指针。
怎么CallbaciImpl
还有继承?从Callback
开始已经跳转两次了还没见到真正的实现,其实这也不远了,CallbackImplBase
说到底就是一个Interface
同样的东西,对CallbackImpl
作了一些限定,这样继承了CallbackImpl
的子类就能以比较一致的方法去操做。其实CallbackImplBase
->CallbackImpl
->各类具体的CallbackImpl
弄这么复杂也是无奈之举。抽出来中间的CallbackImpl
是为了实现多输入参数类型的operator()
的重载,考虑到这个库的编写时间,那时候的c++模板编程好像没有c++11以后的那么完善,没有可变长类型参数,这样作也是无可厚非。我想若是用最新的c++11
以后的标准来写,这个Callback
可能就不会这么难以理解了,彷佛能够直接采用std::function
或者只是作一些小的改动就能够了。无论怎样,到了CallbackImpl
这个层级时,单单CallbackImpl
这个名字就已经能够支撑多达9个入参的operator()
了,再下面层级的类就能够方便的享用这种便利,这也是为何MemPtrCallbackImpl
、FunctorCallbackImpl
等子类能够在一个class里就重载屡次operator()
同时还能作类型检查的缘由了。
TracedValue
追踪先来看看在TypeId
里怎么使用TraceSource
吧!我随便从代码里摘了一条出来
.AddTraceSource ("Tx", "A new packet is created and is sent", MakeTraceSourceAccessor (&BulkSendApplication::m_txTrace), "ns3::Packet::TracedCallback")
又看到了熟悉的Accessor
,这个Accessor
为的就是能拿到类里的一个成员变量。所幸对于TraceSource
来讲,只存在访问Get
而不存在设置Set
,这个Accessor
相比起AttributeAccessor
来讲要简单一些。追到代码里看到的仍是熟悉的的套路:
template <typename T> Ptr<const TraceSourceAccessor> MakeTraceSourceAccessor (T a) { return DoMakeTraceSourceAccessor (a); } // DoMakeTraceSourceAccessor的实现 // 在函数内部定义新的类,这个类实现了TraceSourceAccessor的接口 // 由于`TraceSource`只有一种类型,这种类型就是 // “类内部的成员变量” // 因此能够看到函数的签名就只有一种 // SOURCE T::a // sigh...又是这个鬼畜的标记 template <typename T, typename SOURCE> Ptr<const TraceSourceAccessor> DoMakeTraceSourceAccessor (SOURCE T::*a) { struct Accessor : public TraceSourceAccessor { virtual bool ConnectWithoutContext (ObjectBase *obj, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).ConnectWithoutContext (cb); return true; } virtual bool Connect (ObjectBase *obj, std::string context, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).Connect (cb, context); return true; } virtual bool DisconnectWithoutContext (ObjectBase *obj, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).DisconnectWithoutContext (cb); return true; } virtual bool Disconnect (ObjectBase *obj, std::string context, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).Disconnect (cb, context); return true; } SOURCE T::*m_source; } *accessor = new Accessor (); accessor->m_source = a; return Ptr<const TraceSourceAccessor> (accessor, false); }
TraceSource
存在的理由就是要触发其余的逻辑的,所以要提供挂接其余逻辑的方法,即Connect
。这里的Accessor
只是简单的把Connect
的请求转发给了TraceSource
。好几个类都有Connect
,一不当心就晕头转向了,如今能够总结一下不一样类的Connect
到底作了什么,以及它们究竟时什么时候被调用的。
假设如今已经有一个ns3的类MyObject
(fifth.cc),也继承了Object,意味着它实现了GetTypeId
,拥有了Attribute
和TraceSource
的能力。它有一个能够被trace的值m_myInt
。这个值不是简单的类型,而是一个TracedValue<int32_t> m_myInt;
。这样的话,只要对m_myInt
进行赋值,Trace系统就能够工做了。给这个m_myInt
赋值的话会调用什么?固然是operator=
了。跳到TracedValue
的对应实现看看:
TracedValue &operator = (const TracedValue &o) { TRACED_VALUE_DEBUG ("x="); Set (o.m_v); return *this; } // 关键就在`Set`里了,里面确定有触发`Callback`的代码 void Set (const T &v) { if (m_v != v) { m_cb (m_v, v); m_v = v; } }
果真,有个m_cb
,这就是咱们以前提到的TracedCallback
。每次给这个m_myInt
赋值,就会调用m_cb
通知这个值已经变化。能够看到,TracedValue
自己提供了一个Connect
的方法,这意味着咱们能够直接用m_myInt->Connect
来把本身的处理函数链接上去。可是实际中每每是经过myObj->ConnectWithoutContext("myInt", MakeCallback(&mycallback))
这样的方式。
bool ObjectBase::TraceConnectWithoutContext (std::string name, const CallbackBase &cb) { NS_LOG_FUNCTION (this << name << &cb); TypeId tid = GetInstanceTypeId (); Ptr<const TraceSourceAccessor> accessor = tid.LookupTraceSourceByName (name); if (accessor == 0) { return false; } bool ok = accessor->ConnectWithoutContext (this, cb); return ok; }
Accessor
咱们以前已经讲过了,是一个在GetTypeId
里被调用并生成的一个类型,具体的accessor->ConnectWithoutContext
在上面的DoMakeTraceSourceAccessor
里有定义,仍是经过了SOURCE T::*a
这个类型获得了TracedValue
在类中的位置,调用了这个类型的ConnectWithoutContext
。
至于TracedCallback
,理解起来就没什么难度了,在这个类型的operator()
里,注册进来的Callback
以此执行一遍便可。
因此在运行是整个trace的过程是这样的:
TracedValue
的operator=
TracedValue::operator=
调用了与m_myInt
相关联的TracedCallback::operator()
TracedCallback::operator()
以此调用事先注册好的Callback
。GetTypeId
里AddAttribute
或者AddTraceSouce
一次就至关于给表里加一行记录,全部与Attribute
、TraceSource
相关的操做都会去表里找本身须要的信息。C++真难。
这套代码看下来,也是让人惊叹C++真的是没有作不到,只有想不到。那些模板和宏生成代码的套路,基本上把能在编译期算的都在编译器搞定,能在初始化搞的全在初始化时完成,运行时跑得都是很难再简化的必要的逻辑。其实网络仿真程序也基本算一个"well-defined"的东西,有着明确的需求,又是开源项目,能够花心思把系统设计的如此巧妙。
但愿本身之后能有机会能从头参与相似项目的开发,而不是在反复无常的业务逻辑上消磨时光。