页面

分类

Xirang Array的一个缺陷分析

2015/5/9, by wingfire ; 分类: 计算机技术, 程序设计; 0 comments

最近写了一个程序,用来将Protein的asset对象转换成新的xirang的运行时对象,基本完工。但是,在运行这个转换工具程序的时候,遇到了一个不稳定重现的问题。稍微跟踪了一下代码,发现是在序列化新系统中的一个Array对象时,引用的元素Type无效。

序列化获取Type的过程非常简单,它检测到了问题,但调用栈并不包含问题根源的事发现场。我没有立刻去跟踪代码去定位问题发源地,而是仔细回忆了设计逻辑。

为了抛弃历史包袱和保持新的运行时的单纯性,Protein和新系统分处在两个运行时中,相互是隔绝的。转换程序中有个特别繁琐的地方,就在于要将Protein的asset对象的每个property,逐个地赋值给对应的新系统中对应对象的data member.因为property和data member分属不同的系统,因此,即使它们具有逻辑上相同的数据类型(如int32),其对应的运行时Type也不可能相同(两个int32指向不同的类型)。因此,我在赋值的时候做了一点hack:

    ConstCommonObject src = ...
    CommonObject dest = ...

    ConstCommonObject fake_src(dest.type(), src.data());
    dest.assign(fake_src);

src来自于Protein,dest来自于新系统.这段hack对大多数类型都工作的很好,然而对于Array却是错误的。究其原因,要谈到Array的设计和实现。

Array的设计借鉴了std::vector。std::vector的参数类型是编译时绑定的,一旦绑定就不能改变。对于Xirang来说,其中Type的建立,相当于是在编译期完成的,因此设计成绑定后不能改变。这个体现在Array对应的Type上,其中的类型参数value_type是不会被改变的。但是在实现Array的时候,我犯了几个错误。

  1. Array内部持有元素的Type。这个从设计上来说,是冗余的。要使用Array对象,首先需要以其Type为模板,绑定类型参数value_type,得到一个新的Type.假设value类型为int32,绑定后的类型为array_int32. 对于类型为array_int32的CommonObject来说,内部是不必持有元素类型int32的,因为从array_int32开始,按图索骥是可以知道其元素类型的。但是因为早期CommonObject和宿主C++对象之间交互能力的限制,使得Array对象不能不持有元素类型。
  2. Array的default ctor。为了实现C++的值语义,Array被设计成有默认构造,但是在1的设计下,Array又必须持有元素的Type,而这个Type又必须有效,默认构造无法满足设计1,因此这两个要求是矛盾的。作为妥协,默认构造出的Array状态将是invalid的。
  3. Array的copy ctor中,把参数的元素类型作为自己的元素类型。很显然,copy ctor的参数对象必须和自身处于同一个runtime,这样其持有的元素类型将是本系统的。但是如有违反,copy ctor无法检测到的。
  4. Array的assignment首先要求入参的元素类型和自己相同,其次不应该改变自身持有的类型--这是“绑定”这一涵义自然得到的推论。但是,因为在2中的妥协,存在invalid状态的Array,这时就需要接受并替换自身持有的元素类型,同样,此事无法检测到入参元素对象是否是本系统的。

这次的问题就是在我将一个Protein系统的array_color4对象赋值到新系统中导致的。尽管我在赋值时,通过伪造src的类型(但内存布局相同,因此伪造合法),解决了array本身的类型必须来自同一个runtime的问题,但是此方法无法影响其内部持有的元素Type(color4对应的),导致新系统中的Array对象实际持有了Protein系统Type的引用。当Protein的runtime销毁后,这个Array对象所引用的Type也就无效了。此事,不难得出结论,Array对象持有一个元素Type的引用不是一个良好的设计。

那么,为什么Array存在如此多的不合理设计和实现呢?这又是因为旧的Xirang中CommonObject和宿主C++对象之间交互手段的设计导致的。

在旧的Xirang中,提供了模板函数template <T> T& bind(CommonObject obj)用于将一个Xirang对象绑定成一个C++对象的引用,但是实现上,其间缺少一个中间层的设计,用于返回类型T的代理。在新的Xirang中,插入这样一个中间层所需的支持已经完备,理论上,我们可以借此完美解决前述的设计错误了。

新系统提供了一个模板Ref来得到C++对象的引用,也可以特化成返回一个对象引用的代理,还可以通过偏特化,提供多种返回代理,以适应不同的需求。

因此Ref(obj)可以返回对象ArrayProxy,ArrayProxy的实现大致如下:

    struct ArrayProxy{
           Type value_type;
           void* data;
           // all methods of Array......
    };

这和现有Array的实现几乎一模一样,除了ctor, dtor, assignment, swap等特殊函数。但是却足以将元素Type的引用消除掉,且不会引入任何额外的性能开销。

回顾整个问题。在我最初实现Array的时候,就认识到这个实现不完善,持有元素Type多余。虽然当时也有考虑到多个runtime,但是没有考虑到需要跨runtime转换数据。所以,我当时认为这个主要是实现上的妥协。但是经过对这次的缺陷的分析,我认识到这确实是一个设计缺陷,而不只是一个实现问题。要修正Array本身并不难,但是修正使用了Array的地方,工作量可就不小了。

设计之间存在依赖性,一个设计缺陷可以是另一个设计缺陷导致的。这次的事件再次强化了我的一个观点,那就是发现设计缺陷是不容易的,修正的代价也更高,尽量做出正确的设计,即时调整不合适的设计是非常重要的。流程可能对此问题有帮助。这样的设计缺陷如果有良好的设计审查过程,是极有可能避免的。尽管设计审查也不一定能生效,因为在早期,有效解决这个问题的基础设施就不存在,但是设计审查本身还起到知识传播和交流的作用,因此无论如何不算浪费。

DRY,有些时候叫SPOT,可能不少人知道,但是这些原则不仅适用于函数、类,数据结构的设计同样重要。对于数据重复和冗余,了解数据库设计范式是很有借鉴意义的,尤其是第三范式。另外,从信息出发的分析也是很有效的。逻辑上,Array对象持有的元素类型和Array对应的Type中的value_type必须是同一个,那么设计上,却导致同一个信息分处两处,这就是信息冗余,是不良的设计。

添加评论:

 
 the email would not displayed
 

您可以使用 Markdown 语法。

您必须启用浏览器的 JavaScript 功能才能发表评论。