背景
Bjarne在*Software Development for Infrastructure*中提到过一个因为参数单位不匹配导致重大损失的例子:1999年9月23日,NASA的Mars Climate Orbiter探测器由于导航问题消失在太空中,经济损失达6.5亿美元左右,问题的原因很简单,NASA开发的航天器的基础软件采用国际单位制(SI:the International System of Unit),而洛克希德马丁公司开发的推进器采用的单位是英制单位(Imperial Measurement System),这样,当NASA调用洛克希德马丁公司开发的软件接口时,传递的数值单位为磅力秒(lbf.s),而推进器期望的单位是(N.S),这样,错误的数值传递将航天器推入了茫茫宇宙深处。
问题
void applyMomentumToSpacecraftBody(double impulseValue);
上面的接口中,impulseValue的单位在NASA和洛克希德马丁公司之间没有统一。
怎样解决这种单位不匹配的问题呢?靠文档约定,代码注释?显然这不能解决问题。如果能够在编译期进行单位检查,单位不匹配直接编译失败,就可以从根本上解决这样的问题。C++11中采用Type-rich编程,就可以解决这个问题。
预备知识
类型(type)和单位(unit)
上面的例子中,impulseValue的单位没有统一的根本原因是,它只能表示一个数值(value), 没有单位(unit)信息。我们使用double impluseValue能够表示值的类型(type),如double,也可以表示值的大小, impluseValue对应的数值(value),但是对于单位(unit),比如是kg还是meter还是动量等却无能为力。
如果需要给出单位信息,那么我们可以定义如下的结构体:
template<int M, int K, int S>
struct Unit{
enum { m = M, k = K, s = S};
};
上面的模板定义了一个基于MKS单位体系的物理量。缩写MKS分别表示meter(长度),Kg(重量),seconds(时间),这三种基础单位组合可以表示任何给定的物理单位。
比如,单独的米(长度),Kg(重量),秒(时间)可以如下表示:
Unit<1,0,0> meter;
Unit<0,1,0> weight;
Unit<1,0,1> second;
对于一些组合单位,也可以类似表示:
Unit<2,0,0> squareMeter; // AREA
Unit<3,0,0> cubeMeter; // VOLUME
Unit<1,0,-1> speed; // m/s
Unit<1,0,-2> accleration; // m/s^2
带有单位的值
那么一个带有单位的值,则可以采用下面的模板表示:
template< typename UNIT>
struct Value{
double val;
explicit Value(double d):val(d) {}
};
基于上面的模板,我们可以声明一些常用的带有单位的值的数据结构:
using Length = Value<Unit<1,0,0>>;
using Mass = Value<Unit<0,1,0>>;
using Speed = Value<Unit<1,0,-1>>;
using Momentum = Value<unit<1,1,-1>>;
初步解决方案
那么,对于NASA的问题, 采用下面的sample code, 我们就可以拯救宇航火星探测器了:
#include <iostream>
template<int M, int K, int S>
struct Unit {
// a unit in the MKS system
enum { m=M, kg=K, s=S };
};
template<typename Unit> // magnitude with unit
struct Value {
double val; // the magnitude
explicit Value(double d) : val(d) {} // construct a Value from a double
};
using Momentum = Value<Unit<1,1,-1>>;
void applyAnyValueToSpacecraftBody(double impulseValue) {
std::cout<< "Any value can apply to spacecraft " << impulseValue << std::endl;
}
void applyMomentumToSpacecraftBody(const Momentum& impulseValue) {
std::cout<< "Only momentum value can apply to spacecraft " << impulseValue.val << std::endl;
}
int main()
{
Momentum v(10);
applyMomentumToSpacecraftBody(v);
// applyMomentumToSpacecraftBody(1000); // cannot pass compile
applyAnyValueToSpacecraftBody(10000);
// applyAnyValueToSpacecraftBody(v); // cannot pass compile
}
这样当需要使用动量作为单位时,如果再传入一般的不带单位的数值到函数applyMomentumToSpacecraftBody时,这时候程序直接不能编译通过,可以从源头上避免类似的错误。
解决方案完善
重载运算操作符
当然我们的目标是星辰大海,我们不会止步于上面的解决方案。对于上面初步的解决方案,它有下面的问题:
- 如何对同类型的带有单位的值进行数值运算,加/减/乘/除等
- 如何对不同类型的带有单位的值进行数值运算,加/减/乘/除等
我们应该注意到,对带有单位的值进行数值运算时,我们必须也要考虑到其单位:比如5米+5米=8米;5米*5米=25平方米;5米/1秒=5米/秒,这里,我们必须完善Value类模板:
template< typename UNIT>
struct Value{
static constexpr int getM(){ return UNIT::m; }
static constexpr int getK(){ return UNIT::k; }
static constexpr int getS(){ return UNIT::s; }
double val;
explicit Value(double d):val(d) {}
Value<UNIT> operator +(Value<UNIT> another ){
return Value<UNIT>(val - another.val);
}
Value<UNIT> operator -(Value<UNIT> another ){
return Value<UNIT>(val - another.val);
}
template< typename OTHER >
Value<Unit<getM() + OTHER::getM(), getK() + OTHER::getK(), getS() + OTHER::getS()>> operator * (OTHER other) {
Value<Unit<getM() + other.getM(),getK() + other.getK(), getS() + other.getS()>> result(val * other.val);
return result;
}
template< typename OTHER >
Value<Unit<getM()-OTHER::getM(), getK()-OTHER::getK(), getS()-OTHER::getS()>> operator / (OTHER other) {
Value<Unit<getM()-other.getM(),getK()-other.getK(),getS()-other.getS()>> result(val/other.val);
return result;
}
void print(){
std::cout << "Unit<" << UNIT::m << "," << UNIT::k << "," << UNIT::s << ">" << std::endl;
std::cout << "value: " << val << std::endl;
}
};
这里,我们对代码进行了不少的改进:
- static constexpr版本的getM()等function, static的作用是保证getM()可以通过Value::getM()调用来获取对于的单位;constexpr则是为了保证getM()可以在编译期被调用
- 重载了+,-,*,/操作符:由于这些重载都是模板重载,那么进行数值运算时:对单位的检查在编译的时候已经完成了,对值的计算在运行时完成,因而极大的提高了程序的安全性
重载文字运算符(suffix literal operator)
假设我们声明了
using UnitSecond = Unit<0,0,1>;
using UnitMeter = Unit<1,0,0>;
using Speed = Value<Unit<1,0,-1>>;
using Distance = Value<UnitMeter>;
using Time = Value<UnitSecond>;
上面给出的方案必须显式的定义两个类型再进行运算,如下:
Distance m(3);
Time t(1.2);
Speed s0 = m / t;
如果我想直接进行下面的计算再赋值,是否可行呢:
// Speed s = 3;
// Speed s0 = 3 / 1.2;
Speed s1 = 3_m/1.2_s;
// Speed s2 = 1_m/1.2
由于我们没有定义double到Value<Unit>的拷贝构造函数(设计上也不打算定义,因为如果这么做Speed s = 3这样的调用就会成功,前面的applyMomentumToSpacecraftBody(1000)也会通过编译,就违背了我们的初衷)。
那么我们是否可以像s1那样进行计算呢,其中3_m表示3米,单位是Unit<1,0,0>, 1.2_s是1.2秒,单位是Unit<0,0,1>,这样,速度=距离/时间,看起来就会非常直观,并且类似s2那样的使用也会直接报错。C++11中引入的suffix literal operator解决了这个问题:
Value<UnitSecond> operator"" _s (long double d){
return Value<UnitSecond>(d);
};
Value<UnitMeter> operator"" _m (unsigned long long d){
return Value<UnitMeter>(d);
};
Value<UnitMeter> operator"" _m (long double d){
return Value<UnitMeter>(d);
};
我们在使用3_m时,编译器默认为我们构造一个Value<UnitMeter>对象,同样的,1.2_s会默认构造Value<UnitSecond>对象(注意,我们不能直接使用operator"" s,没带下划线开头的字符都是保留字符,编译器保留了这些字符作为以后扩展使用),完整的代码如下:
#include <iostream>
template<int M, int K, int S>
struct Unit{
enum { m = M, k = K, s = S};
};
template< typename UNIT>
struct Value{
static constexpr int getM(){ return UNIT::m; }
static constexpr int getK(){ return UNIT::k; }
static constexpr int getS(){ return UNIT::s; }
double val;
explicit Value(double d):val(d) {}
constexpr Value():val(0) {}
Value<UNIT> add(Value<UNIT> another ){
return Value<UNIT>(val + another.val);
}
Value<UNIT> operator +(Value<UNIT> another ){
return Value<UNIT>(val + another.val);
}
Value<UNIT> operator -(Value<UNIT> another ){
return Value<UNIT>(val - another.val);
}
template< typename OTHER >
Value<Unit<getM() + OTHER::getM(), getK() + OTHER::getK(), getS() + OTHER::getS()>> operator * (OTHER other) {
Value<Unit<getM() + other.getM(),getK() + other.getK(), getS() + other.getS()>> result(val * other.val);
return result;
}
template< typename OTHER >
Value<Unit<getM()-OTHER::getM(), getK()-OTHER::getK(), getS()-OTHER::getS()>> operator / (OTHER other) {
Value<Unit<getM()-other.getM(),getK()-other.getK(),getS()-other.getS()>> result(val/other.val);
return result;
}
void print(){
std::cout << "Unit<" << UNIT::m << "," << UNIT::k << "," << UNIT::s << ">" << std::endl;
std::cout << "value: " << val << std::endl;
}
};
using UnitSecond = Unit<0,0,1>;
using UnitSecond2 = Unit<0,0,2>;
using UnitMeter = Unit<1,0,0>;
using Speed = Value<Unit<1,0,-1>>;
using Distance = Value<UnitMeter>;
using Time = Value<UnitSecond>;
using Acceleration = Value<Unit<1,0,-2>>;
using Area = Value<Unit<2, 0, 0>>;
using Momentum = Value<Unit<1,1,-1>>;
Value<UnitSecond> operator"" _s (long double d){
return Value<UnitSecond>(d);
};
Value<UnitSecond2> operator"" _s2 (long double d){
return Value<UnitSecond2>(d);
};
Value<UnitMeter> operator"" _m (unsigned long long d){
return Value<UnitMeter>(d);
};
Value<UnitMeter> operator"" _m (long double d){
return Value<UnitMeter>(d);
};
int main(){
Distance m(3);
Time t(1.2);
Speed s0 = m / t;
s0.print();
Speed s1 = 1_m/1.2_s;
s1.print();
Acceleration acc = 8_m/3.3_s2;
acc.print();
Distance d(3);
d = d + d;
d.print();
d = d - 1_m;
// d = d - 1;// can not pass compile
d.print();
Area s = 3_m * 3_m;
s.print();
}
至此,我们通过rich-type编程,使用变量作为函数参数时,在编译期进行单位检查,比较好的解决了火星探测器遇到的问题。
Reference: