ObjectARX开发中的智能指针
张 军 zhang__jun@cableplus.com.cn
■简介
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 C>
AutoPtr(const AutoPtr<C>& other) throw(runtime_error);
template<class C>
operator = (const AutoPtr<C>& 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 |