C++通用物理单位类型实现:仅相同类型转换及相关问题
需求背景
我想在C++里实现一个通用的Unit类型,用来表示电压、电流、功率这类物理单位,而且单位的数值要用分数形式存储,而非浮点数。
已实现的泛型分数类型
我已经完成了泛型分数类的实现:
template <typename REP> class FractionalValue { public: using Value = REP; using Rep = REP; public: constexpr FractionalValue() : n_(0), d_(1) {} explicit constexpr FractionalValue(Value numerator, Value denominator = 1) : n_(numerator), d_(denominator) {} constexpr double toDouble() const { return static_cast<double>(n_) / static_cast<double>(d_); } constexpr Value value() const { return n_ / d_; } constexpr FractionalValue& simplify() { auto gcd = std::gcd(n_, d_); n_ /= gcd; d_ /= gcd; return *this; } constexpr auto num() const { return n_; } constexpr auto den() const { return d_; } private: Value n_; Value d_; };
注:原代码中num()和den()缺少返回值类型,此处补充auto以适配C++17及以上标准。
单位类型设计思路
我对单位的定义是:
"单位是带有额外前缀信息的分数值"
举个实际例子:电压可以用伏特、毫伏、微伏表示,三者可互相转换:1/2伏特 = 500/1毫伏 = 500000/1微伏。
基于这个思路,我设计了通用的Unit类:
template <typename T, typename REP, typename RATIO = std::ratio<1>> class Unit : public FractionalValue<REP> { public: using Ratio = RATIO; public: using FractionalValue<REP>::FractionalValue; template <typename OTHER> constexpr operator OTHER() const { using NewRatio = std::ratio_divide<Ratio, typename OTHER::Ratio>; return OTHER(this->num() * NewRatio::num, this->den() * NewRatio::den); } };
用法示例
我通过以下方式使用该Unit类:
template <typename RATIO = std::ratio<1>> class Voltage: public Unit<Voltage<RATIO>, long long, RATIO> { public: using Unit<Voltage<RATIO>, long long, RATIO>::Unit; }; using Volts = Voltage<>; using Millivolts = Voltage<std::milli>; using Microvolts = Voltage<std::micro>;
目前存在的问题与疑问
1. 单位转换的类型安全问题
当前转换运算符允许所有Unit类型间互相转换,但我需要仅允许同物理量的单位转换——比如毫伏可转伏特,但伏特不能转安培。
2. 泛型数学运算符的实现
不知道如何用泛型方式正确实现加减乘除等数学运算符,同时保留单位的类型信息。
3. 继承 vs 组合的选择
Unit类目前继承自FractionalValue,但继承存在问题:比如调用simplify()方法时,返回的是FractionalValue类型而非实际的Unit类型。想问应该用继承("是一个"FractionalValue)还是组合("包含一个"FractionalValue)?
解决方案
问题1:限制同类型单位转换
要实现类型安全的转换,我们可以给每个物理单位(如Voltage、Current)添加标签类型,然后在转换运算符中用static_assert检查目标类型的标签是否与当前类型一致。
首先修改Unit类的模板参数,加入标签类型:
// 定义物理量标签 struct VoltageTag {}; struct CurrentTag {}; struct PowerTag {}; template <typename TAG, typename REP, typename RATIO = std::ratio<1>> class Unit : public FractionalValue<REP> { public: using Tag = TAG; using Ratio = RATIO; public: using FractionalValue<REP>::FractionalValue; template <typename OTHER> constexpr operator OTHER() const { // 编译期检查:仅允许同物理量单位转换 static_assert(std::is_same_v<typename OTHER::Tag, Tag>, "无法在不同物理单位间转换"); using NewRatio = std::ratio_divide<Ratio, typename OTHER::Ratio>; return OTHER(this->num() * NewRatio::num, this->den() * NewRatio::den); } };
然后修改Voltage的定义,传入对应标签:
template <typename RATIO = std::ratio<1>> class Voltage: public Unit<VoltageTag, long long, RATIO> { public: using Unit<VoltageTag, long long, RATIO>::Unit; }; // 同理定义电流单位 template <typename RATIO = std::ratio<1>> class Current: public Unit<CurrentTag, long long, RATIO> { public: using Unit<CurrentTag, long long, RATIO>::Unit; }; using Amps = Current<>; using Milliamps = Current<std::milli>;
这样如果尝试将Volts转换为Amps,编译时会触发static_assert报错,保证类型安全。
问题2:泛型数学运算符实现
我们需要为Unit类实现运算符,同时根据运算规则保留正确的单位类型。以下是基础实现:
同单位加减运算
template <typename TAG, typename REP, typename RATIO> constexpr Unit<TAG, REP, RATIO> operator+(const Unit<TAG, REP, RATIO>& lhs, const Unit<TAG, REP, RATIO>& rhs) { // 通分计算:a/b + c/d = (a*d + c*b)/(b*d) auto numerator = lhs.num() * rhs.den() + rhs.num() * lhs.den(); auto denominator = lhs.den() * rhs.den(); return Unit<TAG, REP, RATIO>(numerator, denominator).simplify(); } template <typename TAG, typename REP, typename RATIO> constexpr Unit<TAG, REP, RATIO> operator-(const Unit<TAG, REP, RATIO>& lhs, const Unit<TAG, REP, RATIO>& rhs) { auto numerator = lhs.num() * rhs.den() - rhs.num() * lhs.den(); auto denominator = lhs.den() * rhs.den(); return Unit<TAG, REP, RATIO>(numerator, denominator).simplify(); }
单位与标量的乘除运算
template <typename TAG, typename REP, typename RATIO> constexpr Unit<TAG, REP, RATIO> operator*(const Unit<TAG, REP, RATIO>& unit, REP scalar) { return Unit<TAG, REP, RATIO>(unit.num() * scalar, unit.den()).simplify(); } template <typename TAG, typename REP, typename RATIO> constexpr Unit<TAG, REP, RATIO> operator/(const Unit<TAG, REP, RATIO>& unit, REP scalar) { return Unit<TAG, REP, RATIO>(unit.num(), unit.den() * scalar).simplify(); }
不同前缀单位的运算
如果需要支持不同前缀的单位运算(如毫伏加伏特),可以先将其中一个转换为统一前缀再计算:
template <typename TAG, typename REP, typename RATIO1, typename RATIO2> constexpr auto operator+(const Unit<TAG, REP, RATIO1>& lhs, const Unit<TAG, REP, RATIO2>& rhs) { // 转换为基准单位(RATIO<1>)后运算 using BaseUnit = Unit<TAG, REP, std::ratio<1>>; auto lhs_base = static_cast<BaseUnit>(lhs); auto rhs_base = static_cast<BaseUnit>(rhs); return lhs_base + rhs_base; }
问题3:继承还是组合?
推荐使用组合方式,因为继承会导致"切片"问题(如simplify返回FractionalValue而非Unit),且Unit与FractionalValue的关系更偏向"包含一个分数值",而非"是一个分数值"。
修改后的Unit类用组合实现:
template <typename TAG, typename REP, typename RATIO = std::ratio<1>> class Unit { public: using Tag = TAG; using Ratio = RATIO; using ValueType = FractionalValue<REP>; public: constexpr Unit() : value_() {} explicit constexpr Unit(REP numerator, REP denominator = 1) : value_(numerator, denominator) {} // 允许从FractionalValue构造 explicit constexpr Unit(const ValueType& value) : value_(value) {} // 暴露FractionalValue的必要接口 constexpr double toDouble() const { return value_.toDouble(); } constexpr REP value() const { return value_.value(); } constexpr Unit& simplify() { value_.simplify(); return *this; } constexpr REP num() const { return value_.num(); } constexpr REP den() const { return value_.den(); } // 类型安全的转换运算符 template <typename OTHER> constexpr operator OTHER() const { static_assert(std::is_same_v<typename OTHER::Tag, Tag>, "无法在不同物理单位间转换"); using NewRatio = std::ratio_divide<Ratio, typename OTHER::Ratio>; return OTHER(this->num() * NewRatio::num, this->den() * NewRatio::den); } private: ValueType value_; };
这样调用simplify()时返回的是Unit类型,避免了继承带来的类型问题,同时更符合"单位包含分数值"的语义。
内容的提问来源于stack exchange,提问作者Patrick Wright

