找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

查看: 884|回复: 3

[原创]:ObjectARX开发中的智能指针

[复制链接]
发表于 2004-8-14 13:10:05 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
■简介
AutoCAD中所有在dwg文件中保存的对象都从AcDbObject对象中继承,AcDbObject重载了new方法,对象一旦加入到文件数据库(AcDbDatabase)后,内存就由AutoCAD管理,开发着就无法delete它。AutoCAD系统不仅管理对象的内存,还管理对象的undo,深度克隆等,AutoCAD的技术方案是对象的指针被ID(AcDbObjectID)包装,可以打开ID获得指针,使用完毕后关闭,所有的一切内部处理由系统完成,开发需要做的是打开和关闭指针。
从开发者的角度看,打开和关闭实际上就是获得和释放对象的管理权限,一旦打开对象后需要迅速使用以及用完以后马上关闭,否则对于对象的访问冲突将导致AutoCAD程序退出(acrx_abort调用)的严重后果【ARX】。实践证明在开发过程中,由于程序的复杂性以及调试疏忽等原因而导致资源冲突很难完全克服。解决资源冲突的最简单有效的方法是使用智能指针技术【C++】, 智能指针同时与C++的异常很好的配合将使得程序更简洁和可靠,在ARX中智能指针还可以对象的cast、new以及与对象事务管理技术完美结合。
■关键词
ObjectARX 智能指针 对象 ID 异常 cast 事务
1.        错误处理
ARX典型的函数以及对象方法的形式是:
Acad::ErrorStatus call_func(P1 p1, const P2& p2, P3& p3);
其特点就是以一个在结构Acad中定义的枚举ErrorStatus表示函数的执行结果,输入参数为值(P1)或者const引用(P2),而以非const引用(P3)来作为返回值。
这种C类型的函数方式有两个缺陷:
a)        返回的ErrorStatus占据了参数的返回位置。
b)        暗示函数的调用方要检查每个函数的调用结果。结果是在代码中混杂大量错误处理的行,使得程序代码长度成倍增加并且结构陷入混乱。实际中,只会对主要的或者怀疑可能出问题的函数返回结果加以检查(例如打开对象的acdbOpenObject方法)而忽略大部分返回结果。一般情况下这种检查已经足够,然而偶尔也会出现问题,这些问题难以发现和调试,最终成为影响程序的可靠性和稳定性的重要因素。
代码的运行错误可以以抛出”异常”的方式返回,函数的返回值中将放置正常的参数。这就要求调用方能够捕捉可能发生的异常,由于异常处理代码游离在程序的正常代码之外,有利于程序结果的清晰和完整。对于原有的ErrorStatus错误,将被acad_error对象包装:
class acad_error : public std:: runtime_error
{
private:
        Acad::ErrorStatus es_;
public:
        acad_error(Acad::ErrorStatus es) : es_(es) {}
        virtual const char *what() const throw(){
return acadErrorStatusText(es_);
                }
};
what方法中将调用ARX函数acadErrorStatusText返回Acad::ErrorStatus所对应的错误名称。此外可以提供辅助检测函数:
inline check_acad_error(Acad::ErrorStatus es)
{
        if(es!=Acad::eOk) throw(acad_error(es));
}
当程序中异常抛出时,要求所有打开的指针关闭,就是要求所有的资源都用智能指针管理,或者处于的事务管理之下,否则最终将会导致AutoCAD退出。
2.        事务管理
为了解决对象同时打开时的资源冲突以及对象打开的权限冲突问题,ARX的API提供了事务管理对象AcTransactionManager,使用事务可以:
a)        启动事务以后,通过事务打开对象。事务打开对象与使用acdbOpenObject直接打开的对象可以并行而无冲突。
b)        关闭事务时可以使用close方式递交所有的对象变动,或者以cancel方式取消对象所有变更。
c)        支持多层次的事务,可以查询事务的打开层数。打开事务的次数与关闭事务次数必须相等,对象实际是在最外层事务关闭时递交。
d)        可以在对象递交前,刷新对象图形显示。
e)        可以查询事务中管理的所有对象。
f)        可以将新建对象交由事务管理。
由上可知,事务以某种打包的形式管理了一组对象,当这些对象处于事务管理中时无需单独释放资源,而是在事务递交的一刻同时释放。同时事务本身也占据资源,在异常发生时也要释放。为了与异常机制很好的配合,事务也要用智能指针包装。
class AutoTran{
private:
bool commit_;
//禁止对象拷贝,对象仅仅可以拷贝引用
AutoTran(cosnt AutoTran&);
operator = (const AutoTran&);
public:
AutoTran() : commit_(false) {actrTransactionManager-> startTransaction();}
~ AutoTran(){
if(commit_) actrTransactionManager->endTransaction();
else actrTransactionManager->abortTransaction();
}
void commit() { commit_ = true;}
//包装AcTransactionManager其他方法,函数的名称、参数不变
        AcTransactionManager* operator->();
};
AutoTran中含义递交标记commit_,对象构造时设置标记为false。对象构析以前需要调用commit方法,这样在构析时事务被end递交。如果对象是由于异常抛出而构析,就没有机会调用commit方法,事务将被abort。可以用一组宏来包含调用方对于异常的捕捉:
        #define ARXE_TRAN_BEGIN(tr) { using namespace arxe; \
AutoTran tr; AutoTran arxe_tran_tr& = tr; try{
        #defien ARXE_TRAN_END arxe_tran_tr.commit(); \
}catch(const runtime_error& e){acutPrintf(e.what());} }
3.        数据库对象
设计中的智能指针将支持打开所有类型的描述对象,包括对象的ID,ads_name以及对象的句柄AcDbHandle。相对于ID表示当前的AutoCAD进程中所有打开图形对象唯一标记,对象句柄AcDbHandle是图形数据库中对于对象的唯一编号,AcDbHandle于数据库AcDbDatabase相关。
AcDbDatabase可以指向当前的图形,可以用来表示为当前的缺省的图形数据库:
acdbHostApplicationServices()->workingDatabase()
也可以通过打开具体的dwg文件获得,所不同之处是从dwg文件打开的AcDbDatabase必须通过delete来关闭dwg文件,而当前图形数据库是不能delete的。delete也是意味着资源的释放,当与异常机制配合时也需要智能指针技术的帮助。
class Database{
pirvate:
        bool delit_;
        AcDbDatabase* db_;
//禁止对象拷贝,对象仅仅可以拷贝引用
        Database(const Database&);
        operator = (const Database&);
public:
        Database(AcDbDatabase* db = acdbHostApplicationServices()->workingDatabase(), bool delit = false) : delit_(delit), db_(db) {}
        Database(const std::string& name, const int shmode = _SH_DENYWR, bool bAllowCPConversion = false) throw(runtime_error);
        ~Database();
AcDbDatabase* operator->() { return db_;}
};
Database有两种构造方式,如果直接用AcDbDatabase构造,提供当前数据库作为缺省数据库并且设置标记在构析时不删除它。还可以用文件名构造Database,构造参数的含义等于AcDbdababase的readDwg方法,此时将根据文件名称打开数据库,对象构析时删除对象,如果文件无法打开将会抛出异常。operator->()的方法返回可以直接操作的AcDbDatabase*。
4.        对象名
使用acecXXX选择对象返回的是ads_name,该数据结构在ARX的前生ADS中被定义为:
typedef long[2] ads_name;
ADS定义了一组对象、宏来完成ads_name数据结构的初始化、拷贝等。ads_name将被作为智能指针构造参数之一,ads_name也将被对象化。
class AdsName{
private:
        ads_name data_;
public:
        //构造空数值
        AdsName();
        //拷贝构造
        AdsName(ads_name);
        AdsName(const AdsName&);
        //从id构造
        AdsName(const AcDbObjectId&);
        //复制函数
        operator = (ads_name);
        operator = (const AdsName&);
        //转换
        long* asName() const;
        AcDbObjectId asId() const;
}
inline long* asName(AdsName& n){return n.asName();}
考虑对象AdsName与ads_name之间的兼容性,设置asName函数作为转换函数。返回的long*可以直接用在所有ads_name作为参数的地方。同时AdsName支持对象的拷贝,可以用作函数的返回值。
5.        构造智能指针对象
智能指针对象被定义为:
template < class T >
class AutoPtr
{
private:
        T* t_;
public:
......
};
模板参数T为AcDbObject及其子对象。AutoPtr保证对象在构造后形成有效的T*,无法有效构造T的构造过程都将抛出异常。
有以下几种构造对象的方式:
5.1.        指针构造对象
AutoPtr(AcDbObject* obj)throw(runtime_error);
构造过程必须满足下列条件,否则将抛出异常:
a)        obj非NULL。
b)        如果obj可以被T::cast(obj)转换为T,则构造成功。
c)        如果对象无法构造,obj资源将被释放。
5.2.        ID构造对象
AutoPtr(AutoTran& tr, const AcDbObjectId& id,  AcDb::OpenMode mode,
    Adesk::Boolean openErasedObject = Adesk::kFalse)) throw(runtime_error);
智能指针打开对象将强制使用事务,对象被事务打开后获得指针,打开的mode和openErasedObject参数同标准的对象打开方式,打开后的指针按照上述指针构造方式构造。
5.3.        ads_name构造对象
AutoPtr(AutoTran& tr, const AdsName& name,  AcDb::OpenMode mode,
    Adesk::Boolean openErasedObject = Adesk::kFalse)) throw(runtime_error);
name的asId方法将AdsName转换为AcDbObjectId,然后按照ID构造对象的步骤构造对象。
5.4.        句柄构造对象
AutoPtr(AutoTran& tr, const AcDbHandle& handle,  AcDb::OpenMode mode,
Adesk::Boolean openErasedObject = Adesk::kFalse, Database = Database())) throw(runtime_error);
        对象句柄handle将用Database的getAcDbObjectId方法获得AcDbObjectID, 然后按照ID构造对象的步骤构造对象。
6.        获得指针
运算符方法直接返回T。
T* operarot->();
T& operator*();
get函数方法具有参数bool,如果bool==ture,将可能调用upGraduateOpen方法确保返回的T的状态可写。
T* get(bool = ture);
7.        对象的拷贝构造和复制
在智能指针的构造和复制过程中主要解决两个基本的问题:
7.1.        在拷贝中的指针的控制权限。
基本拷贝和复制函数就是:
AutoPtr(const AutoPtr& other) throw(runtime_error);
operator = (const AutoPtr& other) throw(runtime_error);
缺省的拷贝方法是在other对象和当前对象同时拥有T*,对象的两次构析时导致程序的崩溃。但是如果other对象处于事务的管理下,拷贝后的对象T*也处于事务管理之下,是事务对象AutoTran构析时递交对象而不是AutoPtr构析时多次递交对象,那么对象复制以后就不会有问题。
检测对象是否在事务中函数,可以简单调用AcDbObject的同名方法
bool isTransactionResident() const;
拷贝函数是:
a)        判断other是否处于事务中,否则抛出异常。
b)        直接设置当前对象的T*为other->get()。
复制函数是:
a)        用other拷贝构造临时对象tmp。
b)        this对象和tmp对象之间交换T*。
c)        当前对象获得了other对象的T*,当tmp构析时释放原来this中的T*。
T*交互函数为AutoPtr的友员函数:
void exchange(AutoPtr& the, AutoPtr& other) throw();
如果the==other或者the.t_==other.t_,指针的交互不会进行。
7.2.        不同智能指针的转换。
拷贝和复制的对象方法:
template < class G >
AutoPtr(const AutoPtr  < G > & other) throw(runtime_error);
template  < class G >
operator = (const AutoPtr  <  G  > & other) throw(runtime_error);
在这里的转换除了拷贝对象外,还要从C到T转换对象。
拷贝函数是:
a)        判断other是否处于事务中,否则抛出异常。
b)        other->get()方法获得C*。
c)        C*转换为AcDbObject*,用指针构造对象的方法构造T*。
复制函数同上,构造出AutoPtr < T > 临时对象后与当前对象互换。
8.                新建对象
创建对象方式有:
static AutoPtr < T > New(cosnt AutoTran&)throw(runtime_error);
static AutoPtr < AcDbObject > New(cosnt AutoTran&,cosnt std::string& classname) throw(runtime_error);
        当获得对象T的确切类型(定义)后,直接用New创建对象T,新建对象将被加入到事务中。如果没有对象的确切定义,也可以根据对象的classname创建对象。
9.        对象的构析
对象构析时可能面对各种各样的情况:对象可能是新建的也可能已经加入到数据库了,对象可能处于事务或者非事务管理之下,对象可能处于正常的关闭状态,也可以在异常抛出状态。构析的判断过程是:
a)        如果T*==NULL,简单忽略。
b)        如果对象处于事务管理下,返回交由事务管理。
c)        如果对象objectId()==NULL,对象被delete。
d)        如果处于异常状态(uncaught_exception ()==false),对象被close。
e)        如果是新建对象isNewObject(),对象被erase然后close。
f)        对象被close或者cancel。
10.        实例
a)        选择、删除对象。
                AdsName name;
ads_point ptres;
                if(acedEntSel(“\n删除对象:”, name.asName(), entres)!=RTNORM) return;
ARXE_TRAN_BEGIN(tr)
                AutoPtr<AcDbEntity>(tr, name, Acad::kForWrite)->erase();
ARXE_TRAN_END
b)        获得曲线的长度
                AdsName name;
ads_point ptres;
                if(acedEntSel(“\n获得曲线长度:”, name.asName(), entres)!=RTNORM) return;
ARXE_TRAN_BEGIN(tr)
                AutoPtr<AcDbCurve> c(tr, name, Acad::kForRead);
                double param, dist;
                acad_check_error(c->getEndParam(param));
                acad_check_error(c->getDistAtParam(param,dist));
                acutPrintf(“\n曲线的长度为:%f”,dist);
ARXE_TRAN_END

■参考文献:
【ARX】ObjectARX Developer’s Guide——AutoDesk 开发资料,随ARX开发工具包提供
【C++】C++编程语言 B.J

张 军         zhang__jun@cableplus.com.cn
论坛插件加载方法
发帖求助前要善用【论坛搜索】功能,那里可能会有你要找的答案;
如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子标题加上【已解决】;
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【D豆】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
发表于 2004-8-14 15:32:40 | 显示全部楼层
不错,若能在智能指针中引入COM中常用的概念“参考计数”这个指针管理机制的话,则智能指针在ARX开发中的引入,会使程序的可靠性、复杂度以及可维护性等方面得到很大的提高!
论坛插件加载方法
发帖求助前要善用【论坛搜索】功能,那里可能会有你要找的答案;
如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子标题加上【已解决】;
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【D豆】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 支持 反对

使用道具 举报

发表于 2004-8-15 09:14:25 | 显示全部楼层
不错的文字,不过cad好像已经有这样的类,具体参考acdbObjectpointer
论坛插件加载方法
发帖求助前要善用【论坛搜索】功能,那里可能会有你要找的答案;
如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子标题加上【已解决】;
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【D豆】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 支持 反对

使用道具 举报

 楼主| 发表于 2004-8-15 10:17:07 | 显示全部楼层
acdbObjectpointer是对对象一种简单包装,存在一些使用缺陷:
a)每当打开对象必须要检测,检测代码导致程序结构混乱。当引入C++的异常错误之后,系统所有的具有资源性质的指针都需要被包装,也就是讲此时的智能不单单是单个对象问题,需要分析系统所有可能发生资源泄漏的对象。
b)智能指针对于对象的拷贝复制处理问题。简单的复制将导致多个对象同时包含指针,在构析过程中导致系统崩溃。解决问题方法其一是禁止复制,但智能指针作为函数参数传递和参数的返回是正常要求。还有的办法是采用计数,复制对象后所有对象包含指向相同内存的指针,复制导致该位置的计数加一,对象的构析计数减一,计数为0关闭对象,可以参照effective c++。但AcDbObject对象具有多种状态和ACAD内部特殊的内存管理方式,处于new状态,获得了ID但还没有关闭过,用acdbOpenObject方式打开,用transaction打开,或者new后的对象加入到事务中,对象的正常构析以及处于异常构析状态,使得控制方式异常复杂。这里的要求是对象的复制必须处于事务管理状态下,要求对于对象的所以操作在事务下完成,事务具有重要优点,参见objectArx的帮助文件,只要将对象都放置在事务中一切都OK。
c)将cast过程包装在智能指针中。arx编程中的cast是经常使用的方法,dbx对象的AcRxClass在类表中注册以后,总是包含它父对象的的指针,cast过程中根据对象的AcRxClass类一直往上搜寻很快就判断是否可以找到父对象,来决定返回NULL,和或者(T*)。按照现在的C++标准98,C++支持RTTI在语言中就可是实现上述功能,但无论ARX还是MFC都还保留着这些相互之间不兼容的cast。智能指针包装的构造,拷贝构造,复制函数,通过模板函数隐含的实现不同指针之间的cast,无须在代码中显式的处理和判断。
d)虚构造对象。moer effective c++中讲述了虚构造和虚拷贝对象概念和实现方法,AcRxObject::clone就是虚构造对象,但该函数返回的是AcRxObject然后再cast对象到T,现在C++标准是返回T的函数也是可以的。虚构造和虚拷贝使得调用函数方再不知道对象具体类型时,也可以拷贝和新建对象,比如当你得到一个AcDbObject*,你无须知道到底是AcDbLine还是其他的对象,调用clone以后总是可以获得同样的对象,deepclone是一个另外要研究的问题。虚构造对象就是根据对象的类名classname,查找ACAD系统类表中对应的AcRxClass对象,调用该对象的create方法获得对象,调用方始终仅仅知道的对象的类名,没有知道对象的确定类型,就是没有inclue该类的head文件,类的变动重新编译连接不会对与当前模块有任何影响。这里的New方法:
static AutoPtr < AcDbObject > New(cosnt AutoTran&,cosnt std::string& classname) throw(runtime_error);
就是体现的虚构造概念,构造对象过程可能由new抛出异常,或者classname对应类没有注册的异常。新建立的对象被加入到事务中,一旦事务被cancel而不是close,对象被自动delete。
e)基于智能指针,事务管理数据更重要的特征是可以与STL很好的融合,以创建更高效和易于扩展的系统来。
论坛插件加载方法
发帖求助前要善用【论坛搜索】功能,那里可能会有你要找的答案;
如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子标题加上【已解决】;
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【D豆】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|申请友链|Archiver|手机版|小黑屋|辽公网安备|晓东CAD家园 ( 辽ICP备15016793号 )

GMT+8, 2024-11-16 00:53 , Processed in 0.210099 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表