You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++通用物理单位类型实现:仅相同类型转换及相关问题

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 08:09:52