C++11 Type-rich编程

背景

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:

  1. https://blog.csdn.net/u010333737/article/details/98535910
  2. https://www.codeproject.com/Articles/723900/Type-Rich-Style-for-Cplusplus11
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值