对于程序编写人员来说,单纯的数字处理相对要容易一些,而当涉及单位换算时却麻烦得多,一不小心就会出错。本文从数据类型出发,介绍用C++++构造单位型数据对带单位的数据进行跟踪、转换及处理,以减少错误的发生。
长久以来,人们一直用手工进行数学计算,这不仅浪费时间并且很容易出错。后来人们发明了计算器,使数据处理变得相对容易了一些。经常和测量打交道的人也有一个类似的问题:单位换算,我们目前使用数以百计的单位表示各种物理量,如长度、时间和质量等等,它们之间的转换也是一项既费时而又容易出错的工作。幸好现在有了科学计算器可以帮助我们做一些这类工作,如惠普的HP48s能在同类单位(英尺和米或秒和小时之间)间相互转换。这种装置在进行加减或转换之前还会检查单位是否匹配,有了HP48s,人们计算时再也不用担心单位转换是否正确。
那末为什么我们说在软件业中还存在这类问题呢?这是由于软件开发人员使用的是整数和浮点数,并依靠严格的文件、可变标识结构或编码标准来确保数字的准确性。下面是一个典型的代码段:
double height_in_feet=6.0;
double clock_time=10.0;//分钟
但如果要将一种单位转换成另一种单位,程序员们还是得用手工方法来完成,他们先要确定转换系数并确认这些系数正确,这项工作同样又费时又容易出错,程序员们感到困扰的最大原因是单位要比数字更难于处理。下面我们介绍如何使用C++建立单位型数据简化单位转换,这种方法既有效且不容易出错,更重要的是还便于阅读。
规范用语
在建立单位型数据之前,有必要先定义几个术语。
?标量 指任何没有标识的数字,如5、2.16、-5/9、p、√-1、3.1313等等,如果学生在回答实际应用题时只用一个标量作为答案,他们一般都得不到高分。
?单位 就是附在标量后面的标识,这也正是学生回答应用题时常常会忘记的。单位可以分成很多类,如米和英尺属于“长度”类,而秒、分和小时则属于“时间”类。
?测量值 就是将标量和单位合在一起来表示的一个数量。测量结果和任何具体单位没有关系,不管采用英寸、英里、公里或光年表示,两点之间的距离是不会变化的。对于一个测量值来说,标量和单位缺一不可。
假如你住在芝加哥,那么:
测量值=标量×单位
到旧金山的距离(XSF)=2,129×英里
到迈阿密的距离(XM)=2,213×公里
计算机对于标量的表示非常在行,但却不擅长表示测量值。因为英里或公里是标准单位,计算机不需要把这些表示出来,它只要简单地说“英里”我们就知道是什么意思。然而,到旧金山的距离并不是谁都知道,要表示这段距离,我们要将XSF和一个标准单位(比如说英里)作比较,发现它是这个单位的2,129倍;当我们将XM和公里比较时,发现它是这个单位的2,213倍。整理可得:
测量值/单位=标量
或者
XSF/英里=2,129
换一句话说,到旧金山的距离是英里这个标准单位的2,129倍。虽然标量显示出来的数都很清楚,但却不能用来比较测量值的大小。如果我们只是拿上面的标量来比较,很可能会错误地认为芝加哥到旧金山的距离比到迈阿密的要近。因此我们一定要用测量值进行比较和计算,绝对不能只用标量。
现在我们开始来构建单位型数据,一个好的单位型数据应具备下列特征:
?单位转换可以自动进行
?软件应能检查单位是否正确
?数据类型容易使用并且不会用错
?数据处理效率高
自动转换
自动单位转换包括两个步骤。第一步是将单位转换系数和单位标识直接联系起来,暂时我们用一个简单的typedef来表示;第二步是只要可能就用测量值替代标量,在使用测量值时,无论何时都要加上单位标识及转换系数。
图1显示了如何定义长度类型及各种长度单位,然后用这些进行自动转换。这段代码有几个有趣的地方,首先,它不管输入的测量值是什么样的形式,英里、公里或英尺都可以,当要表示一个值的时候,我们将测量值和选定的单位进行比较,然后留下我们希望的标量。其次就是代码本身,它不需要单位注释。当你读这段代码的时候,对芝加哥到旧金山有多远还有疑问吗?对英尺和英里的对应关系有问题吗?最后,它定义米(米=1.0)为常数,当然也可以定义任何一个正数,此时我们只需将米定义行换成一个任意的数字即可,不会影响其它程序,例如:
const Length Meters=123.456
虽然上面的例子并不能解释清楚物理含义,但测量值确实是服从于物理定律的,它们是“纯粹”的数值,使用前不需要转换。在图2的例子中,SoftballSpeed(垒球速度)和CarSpeed(汽车速度)虽然用的是不同单位,也可以直接进行比较。
开发人员应记住,一定要用保存在单位常数中的转换系数进行乘除,此时这种简单的系统才可以工作得很好,将typedef换成C++的类(class)后,它每次运行都将非常完美。
单位检查
为了确保单位正确,我们必须追踪单位或度量的种类(长度、速度、力等等)。目前我们所采用的标量还不知道是属于哪一类,因为这里的单位仅仅只是双精度数,所以有可能会错误地使用不同单位的数据,并且发现不了错误。例如:
//各类都是双精度数
Length x=5.0*Feet;
Time t=3.4*Seconds;
//概念错误,但却不会通知出错
Velocity v=x+t;
为了能自动检查出这类漏洞,系统必须明白基本量和导出量的关系。基本量是最简单的单位类型,它们不可以再分解,包括质量、长度和时间等。用乘法或除法将一个或几个基本量合在一起就产生了导出量,这些导出量完全可以分解成组成它们的基本量。表1就是几个导出量和组成它们的相应基本量,每个基本量上面的指数非常重要。
从表中可以看出,任何类单位都可利用所使用的基本量及其指数唯一确定。将基本量指数与标量转换常数合在一起,就可以自动检测出单位使用中出现的错误。
图3表示单位分类情况,注意这里与每个基本量相关的指数都保存在对象中,作为数据之一。
除了大量分配运算符外,还需要指定几个灵巧的指数算符。新的乘法和除法算符和基本量一起形成导出量,乘法运算使指数增加而除法运算使指数减少;加法、减法和其它各种比较符通过维护指数相等来检查单位类型是否匹配。
易用且不会出错
图4中的标量类型转换是最终也是最重要的一个算符,它可使单位型数据及其它C++功能和数据类型发生联系。在返回标量前通过保证所有指数为零,类型转换强迫程序员将标量从测量值中分开。
这项功能可以防止存取任何有单位的标量,只有在用同一类单位去除测量值时指数才会为零,函数也回到标量;如果指数不为零,可以肯定代码里还有问题。图5就是一个含有这种漏洞的代码示例,这里我们忘记了用括号将Feet/Seconds括起来,幸好时间指数还没有取消,所以我们在运行时得到出错报告。
因为测量值不能用于标准C++函数,它们必须先通过类型转换这一关成为标量。如果代码中有错误,我们也可以在这里发现。
也许你现在会问:“那有什么办法可以防止不小心把米和厘米相加呢?”回答是:“没有办法”。事实上,如果可以的话希望你还是试一下。两者都是长度单位,由于单位标识已经按一定的比例处理过,所以是允许它们相加的。例如要想将300米和75厘米相加,代码就可能会是这样:
Length len=300.0*Meters+75.0*Centimeters;
假设长度的基本单位是米,汇编程序将这行代码解释为:
Length len=300.0*1.0+75.0*0.01;
或者
Length len=300.75;//基本单位:米
如果要将长度用公里表示,那就要再除以千米:
cout<<(double)(len/Kilometers)<<"km"<
同样,将单位换成相应的标量单位:
cout<<(double)(len/1000.0)<<"km"< 可以输出正确的数值0.30075km。
现在所有的语句已经有了,可以将它们组成一个完整的代码段。图6是计算某个液压马达吸收压力后所产生转矩的代码。首先,注意MotorTorque()没有用转换常数时是怎样进行计算的,计算并不知道具体的变量调用程序会用什么单位,也不关心是什么单位,MotorTorque()就像其物理推导式一样易于读懂和验证。其次,由于双精度数并不像单位变量那么可靠,其范围要尽可能地小,使得标识(如PSI或牛顿米)尽量接近双精度数据源(或目的)。在变成单位数据前,用一行代码使吸收压力保持为双精度数,而输出则到使用时才会成为双精度数。
数据有效性
将单位型数据进行分类确实很好,但还是有些问题。它需要对指数进行额外的说明,从而使简单的算术符变成冗长的功能调用。正因为此,如果系统资源有限时采用这个方法就不是很好。所以在完成调试后,我们可能想将#define开关翻转,将单位型数据恢复到typedefed标量,如图7中的units.h。
现在我们来介绍一下用宏CategoryBase()来定义基本单位,将指数留下用于调试软件中的错误,在正式发布时再将其舍弃。同样要注意CategoryBase()和单位型数据的构造要素需要用指数值而不是标量,这是因为任何类型标量的基本单位总是1,其它单位类型常数应该由基本单位乘以某个常数得出。这样一来迫使程序员按照基本单位类型的要求来定义常数,而不能随便创造常数。
单位型数据类也很容易保留这个方法,很可能只有基本单位种类会改变。假设有些项目开始时单位常数按以下方法定义:
CategoryBase(Meters, 0, 1, 0)
const Length Feet=0.3048*Meters;
const Length Inches=Feet/12.0;
在项目当中,又决定(不管什么原因)将长度基本单位由1m改为20cm,这时我们不用对每一个常数进行改变,只需要改变一行代码并增加另一行代码就行了:CategoryBase(U_20Centimeters, 0, 1, 0)
const Length Meters=U_20Centimeters*5.0;
虽然改变了存在标量中的值,但是应用程序的其余部分并没有受到影响。如果只有几个单位常数的话,这样做也许还不显得很重要,但是当有很多单位常数的时候就非常有用。
现在发布的软件中,由于单位型数据都被双精度型替代了,所以大多数计算在汇编程序中就已提前完成。例如在汇编的时候,汇编程序将
Length Height=5.0*Feet+11.5*Inches;
转换为:
double Height=1.816 // 米
整数算法
为简化起见,前面的例子使用的是便于用在等式中的单位作为类型基础,并将其作为双精度数存储。在台式机中它确实可以很好地工作,但在嵌入式系统中却不尽人意。嵌入式系统经常只能做整数运算,测量值很少能方便地用于等式中,但通过简单变换标量数据类型和类型基础,单位型数据几乎可以处理所有的情形。我们以汽车速度计为例来进行说明。
假设汽车速度计采用轮胎脉冲捡拾单元(PPU)来测量距离,通过设计使汽车每行驶一英里产生1,293个PPU脉冲,速度计用一个内部时钟中断器来测量时间,该中断器每隔20ms(50Hz)触发一次。利用这些条件,我们在图8中列出一种在美国销售的汽车上使用的程序代码段。
我们希望ReportMPH()得出一个整数,它得到的也确实是一个整数,尽管代码最后一行并没有出现我们所预期的结果(测量值/单位)。同整数运算一样,我们要注意运算的次序。将除法运算保留用于最后,我们可得到非常精确的答案;同样,我们也要避免用整数除法来定义常数。
//整数除法造成的截断误差
const Velocity MPH=Miles/Hours;
程序模板
模板似乎是C++语言中最不受欢迎的特性。由于很少有汇编程序完全遵照标准,所以模板也没有落得好名声,汇编程序A中的模板也许并不能用在汇编程序B中。
目前我们可以看到,单位型数据的局限性在于:
?需要额外的空间处理指数运算
?指数需要经常检查
?由于功能调用,运算速度减慢
?只用在运算的时候才能检查到错误。
由于模板将指数作为模板参数而不是作为变量,因此上面提到的问题也是可以避免的:
template< int MassExp, int LengthExp, int TimeExp>
class Units {
public:
//下面是算符
private:
double m_Value;
};
typedef Units< 1, 0, 0> Mass;
typedef Units< 0, 1, 0> Length;
typedef Units< 0, 0, 1> Time;
如果将所有算符排列好,标准的乘法、加法和比较运算可以像标量等式一样快。
由于模板自变量不占用内存,所以单位型数据的大小和内部标量一样大,只需用有效的自变量来定义算法就可以保证指数正确,而不需要不停地用assert()检查指数是否匹配。
对任何种类的单位组合,乘法运算都是有效的:
//乘法形式
inline template Units operator*(Units 加法和比较运算要求两个自变量种类匹配:
//加法和比较形式
inline template Units inline template bool Units 标量类型转换只有在指数为零时定义:
//标量类型转换形式
inline Units<0,0,0>::operator double() const;
所有这些运算符经过仔细定义,意味着汇编程序现在在汇编的时候就可以发现错误,而不用等到运行的时候。再重申一次:可以在汇编的时候就发现错误。
如果你的目标系统只有C汇编程序,这项特性就特别有用。你可以先随便用什么模板检查单位,例如C++汇编程序(像GNU的免费的g++),然后简单地将#define开关转换并用自己的目标C汇编程序进行汇编。
如果你的程序采用模板汇编,就可以保证单位正确,而且单位转换也正确,等式也是成立的;总之完全没有运行时间无效的情况,不需要等待声明,也不需要在充斥着很多错误的系统中寻找单位错误,换句话说,就是不会把磅当成牛顿。
作者:Christopher Rettg 系统工程师 Vermeer Manufacturing Email:rettigcd@