【C++代码整洁之道】Type-Rich编程——利用编译器保障类型安全

本系列文章均整理自《C++代码整洁之道——C++17可持续软件开发模式实践》

Type-Rich 编程

不要相信名字
而要相信类型
因为类型不会说谎
类型是你的好朋友
—— Mario Fusco

1999年9月23日, NASA 失去了它的太空探测器 Mars Climate Orbiter I. 这次事故的直接原因可以归结于参与这项工程的两个工作小组一个使用 国际单位制 作为参数单位, 而另一个小组使用 英制单位. 两者的系统各自独立都可以正常工作, 而最后的结果则暴露出软件开发中一个经常被忽视的环节——沟通. 虽然这次事故的重点不在于类型不匹配, 但是这次惨痛的事故恰恰可以说明 Type-Rich 编程的价值.

在我看来, Type-Rich 编程不一定在所有场合都是必要的, 不过可以想象在涉及到各种物理量的系统中, Type-Rich 编程一定是大放异彩的. 这种编程思想我认为是值得思考的, 接下来我们就来探讨何为 Type-Rich 编程以及 Type-Rich 编程的价值.

一种无设计感的接口

我们几乎无时无刻不在设计这样的接口:

class SpacecraftTrajectoryControl
{
public:
    void applyMomentumToSpacecraftBody( const double impulseValue );
};

这样的接口设计可以做到让程序正确运行, 但是隐形中这个接口将一个责任抛给了接口的使用者:

接口的使用者需要自行保证参数的物理含义符合接口内部的实现 !

这里需要注意的是: 一个基础类型 double 不能提供任何信息. 我们需要的实参值到底是什么单位, 使用什么单位制都是无法确定的. double 是一种数据类型, 但是他不是一种语义类型.

一种解决方案是我们可以将形参名修改为 impulseValueInNewtonSeconds, 这是一种比什么都不做好一点的方法, 但是本质上并不能避免用户传入一个错误单位的值.

可能另一种更加常见的方法是给接口添加注释, 但其实这种做法的问题是和修改参数名一样, 而且在原书中的前几章作者也对注释做了一定的说明. 结合笔者自身的工作实践, 项目代码对注释的容忍度应该非常低, 我们更倾向于代码本身就是最好的注释, 代码应该是自解释的, 当你必须使用一段注释来解释代码时可以被认为这就是一种 坏味道.

Type-Rich 编程下的接口设计

那么我们如何做的更好的一点? Type-Rich 编程来了 !

class SpacecraftTrajectoryControl
{
public:
    void applyMomentumToSpacecraftBody( const Momentum& impulseValue );
};

我们使用明确定义的 Momentum 类型来作为接口参数的类型, 而不是一个没有语义价值的 double. 这时一些我们不期望的非法调用将导致编译错误.

SpacecraftTrajectoryControl control;
const double someValue = 15.56;
control.applyMomentumToSpacecraftBody( someValue ); // 编译时报错!
Force force { 15.56 };
control.applyMomentumToSpacecraftBody( force ); // 编译时报错!
Momentum momentum { 15.56 };
control.applyMomentumToSpacecraftBody( momentum ); // 正确做法!

其实这就是 Type-Rich 编程, 其最大的意义是: 类型安全在编译期间即得到保障! 另外统一运用这种方法也可以避免程序逻辑中出现无意义的数字运算.

换句话说, 我们应该在很大程度上避免在公共接口 API 中使用通用的, 底层的内置类型, 比如 int, double, 或者最坏的 void* 等等.

在 C++ 下针对物理量的 Type-Rich 编程

上面其实已经阐述完了 Type-Rich 编程的核心思想, 可以看出其并不复杂, 本质上就是利用编译器来保证正确性, 编译器是不会说谎的~ 下面是在 C++ 下针对物理量进行的一次 Type-Rich 编程实践.

首先我们定义一个模板来表示基于 MKS 单位体系的物理量. 缩写 MKS 分别表示米(长度), 千克(质量)和秒(时间). 这三种基础单位组合起来可以表示任何给定的物理单位. 另外我们还需要一个表示值的类模板.

template <int M, int K, int S>
struct MksUnit
{
    enum { metre = M, kilogram = K, second = S };
};

template <typename MksUnit>
class Value
{
private:
    long double magnitude{ 0.0 };

public:
    explicit Value( const long double magnitude ) : magnitude( magnitude ) { }
    long double getMagnitude( ) const { return magnitude; }
};

接下来我们就可以用这两个类来定义具体物理量的别名了

using Length = Value< MksUnit<1, 0, 0> >;
using Area = Value< MksUnit<2, 0, 0> >;
using Time = Value< MksUnit<0, 0, 1> >;
using Speed = Value< MksUnit<1, 0, -1> >;
using Force = Value< MksUnit<1, 1, -2> >;
using Momentum = Value< MksUnit<1, 1, -1> >;
// 等等

同时可以使用 constexpr 关键字实现 Value 类的编译时计算, 而且在实现了必要的运算符重载之后, 不同单位间的计算也成为可能. 具体实现不再赘述, 用法如下.

constexpr Momentum impuseValueForCourseCorrection = Force{ 30.0 } * Time{ 3.0 }; // 避免 double impuse = 30.0 * 3.0;
SpacecraftTrajectoryControl control;
control.applyMomentumToSpacecraftBody( impuseValueForCourseCorrection );

利用 C++ 新特性的 trick

在现代 C++ 中, C++ 11 之后我们可以为文字提供自定义后缀来为他们定义特殊的函数, 也就是所谓的 文字操作符. 一旦定义了文字操作符之后, 就可以像下面的代码中那样使用他们. 这种形式对于领域专家来说更加友好, 也更安全.

constexpr Time operator"" _s(long double magnitude) {
    return Time( magnitude );
}

Time time = 10.0; // 编译时报错!
Time time = 10.0_N; // 编译时报错!
Time time = 10.0_s; // 正确!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值