C++-分数泛型模板类实现

内容目录

前言

在引擎库中,有时候可能会需要准确描述精度,如果用平常的 float 或者 double 类型,很容易精度不够。
所以在这里我需要实现一个分数类,使其可以描述精确的值,并支持用户自定义类型。

实现

定义

template <typename T>
class Fraction
{
    using ElementType = T;
    T Numerator;
    T Denominator;
public:
    Fraction() {}
    Fraction(const ElementType& NewNumerator, const ElementType& NewDenominator) 
        : Numerator(NewNumerator), Denominator(NewDenominator) {}
    Fraction(ElementType&& NewNumerator     , const ElementType& NewDenominator)
        : Numerator(std::move(NewNumerator)), Denominator(NewDenominator) {}
    Fraction(const ElementType& NewNumerator, ElementType&& NewDenominator)      
        : Numerator(NewNumerator), Denominator(std::move(NewDenominator)) {}
    Fraction(ElementType&& NewNumerator     , ElementType&& NewDenominator)      
        : Numerator(std::move(NewNumerator)), Denominator(std::move(NewDenominator)) {}

    Fraction(const Fraction& NewFraction) = delete;
    Fraction(Fraction&& NewFraction) = delete;

    Fraction& operator= (const Fraction& NewFraction) = delete;
    Fraction& operator= (Fraction&& NewFraction) = delete;

    ~Fraction() = default;

    ElementType& GetNumerator() { return Numerator; }
    ElementType& GetDenominator() { return Denominator; }

    const ElementType& GetNumerator() const { return Numerator; }
    const ElementType& GetDenominator() const { return Denominator; }
};

老规矩,在定义一个较为完善的类之前,需要提前定义多种类型的构造函数,复制构造函数,移动构造函数,复制赋值运算符,移动赋值运算符,析构函数。
将暂时不需要的函数简单的 delete 掉,当被调用时编译器会给我们明确的报错,防止他的默认实现出现问题。

接下来定义两份得到分子分母的 getter,在这里要分别有 const 类型和非 const 类型,供用户选择。

这里的构造函数是临时的,在介绍完最大公约数后需要在构造的时候对分数进行自动化简,防止溢出。

实现复制与移动

由于分数类并没有任何需要他自身管理的内存,所以遇到类型时可以直接复制它的类型。

Fraction(const Fraction& NewFraction) = default;
Fraction(Fraction&& NewFraction) = default;

Fraction& operator= (const Fraction& NewFraction) = default;
Fraction& operator= (Fraction&& NewFraction) = default;

~Fraction() = default;

最大公约数

约数也就是因数,指的是可以被整数最大整除的数,例如 2 可以被 12 整除,所以 212 的一个约数,而最大公约数是两个整数共有约数最大的一个。
例如 1216 的最大公约数是 4,因为 1216 可以同时被整除的最大数就是 4
为了计算最大公约数,可以使用 GCD 函数,具体实现方式如下

int Gcd(int A, int B) {
     return B == 0 ? A : Gcd(B, A % B);
}

这是一个欧几里得算法,如果 BA 的约数,那么 AB 的最大公约数就是 B(因为比 B 再大的数无法被 B 整除。),
如果不是,那么 AB 的最大公约数与 BA % B 的最大公约数相同。

如果第一次没有找到最大公约数,则进行一次递归,将 B 作为函数的参数 A,将 A % B 作为函数的参数 B
再次进行,若已经没有余数(B == 0)的情况下,则最大公约数为参数A(上一个步骤传入的 B)。

而在分数中,主要是为了找到分子和分母的最大公约数,同时将分子与分母除这个最大公约数,从而对分数进行化简。
同时还有一个优点,若是所有的分数均为最简形式,则在比较时无须再次化简。

template <typename T>
static T Gcd(const T& A, const T& B)
{
    return B == 0 ? A : Gcd(B, A % B);
}

这里在外面写一个通用的实现。

加法

为了让模板更加通用,不一定需要两个类型完全一样的分数才可以进行相加。
所以在这里需要将返回值定义为 decltype(auto) 让他计算一个期望的结果。
同时,需要加入 requires 使其在编译期时查找 A + B A * B 是否可用,以确保我们可以得到精确的 debug。

    template <typename OtherElementType> requires (requires (const ElementType& A, const OtherElementType& B) { A + B; A * B; })
    [[nodiscard]] friend constexpr decltype(auto) operator+ (const Fraction& ThisFraction, const Fraction<OtherElementType>& OtherFraction)
{
    T NewNumerator = ThisFraction.Numerator * OtherFraction.Denominator + ThisFraction.Denominator * OtherFraction.Numerator;
    T NewDenominator = ThisFraction.Denominator * OtherFraction.Denominator;
    return Fraction(NewNumerator, NewDenominator);
}

分数由于不能直接相加,所以需要将分母变成相同的,也就是说需要将两个分母相乘,使分母相同,并需要将两个分数的分子分别乘以对方的分母再相加。
由于这样的结果可能会变的很大,所以需要使用刚才说到的 Gcd 函数对分子分母进行化简,而化简的操作可以直接定义在构造函数中,确保每个分数被构造的时候就是化简形式。

化简

void Simplify()
{
    if (Denominator == 0)
    {
        assert(false);
    }
    auto NewGcd = MathAlgo::Abs(MathAlgo::Gcd(Numerator, Denominator));
    Numerator /= NewGcd;
    Denominator /= NewGcd;
}

定义一个化简函数,化简函数需要在构造函数中调用,也可以在一些情况下手动调用。
先取分子与分母的最大公约数,并同时用它们除最大公约数,则分子与分母没有除了1以外的公共因子。
值得一提的是,这里需要取最大公约数的绝对值,防止分子为负数时计算混淆。

在构造函数中调用它。

Fraction()
{
    Denominator = 1;
    Numerator = 0;
}

Fraction(const ElementType& NewNumerator, const ElementType& NewDenominator)
    : Numerator(NewNumerator), Denominator(NewDenominator)
{
    Simplify();
}

Fraction(ElementType&& NewNumerator     , const ElementType& NewDenominator)
    : Numerator(std::move(NewNumerator)), Denominator(NewDenominator)
{
    Simplify();
}

Fraction(const ElementType& NewNumerator, ElementType&& NewDenominator)
    : Numerator(NewNumerator), Denominator(std::move(NewDenominator))
{
    Simplify();
}

Fraction(ElementType&& NewNumerator     , ElementType&& NewDenominator)
    : Numerator(std::move(NewNumerator)), Denominator(std::move(NewDenominator))
{
    Simplify();
}

可以看到已经自动化简了。
file

减法

template <typename OtherElementType> requires (requires (const ElementType& A, const OtherElementType& B) { A - B; A * B; })
[[nodiscard]] friend constexpr decltype(auto) operator- (const Fraction& ThisFraction, const Fraction<OtherElementType>& OtherFraction)
{
    T NewNumerator = ThisFraction.Numerator * OtherFraction.Denominator - ThisFraction.Denominator * OtherFraction.Numerator;
    T NewDenominator = ThisFraction.Denominator * OtherFraction.Denominator;
    return Fraction(NewNumerator, NewDenominator);
}

和加法几乎一致,只需要对符号进行一下修改即可

乘法

template <typename OtherElementType> requires (requires (const ElementType& A, const OtherElementType& B) { A * B; })
[[nodiscard]] friend constexpr decltype(auto) operator* (const Fraction& ThisFraction, const Fraction<OtherElementType>& OtherFraction)
{
    return Fraction(ThisFraction.Numerator * OtherFraction.Numerator, ThisFraction.Denominator * OtherFraction.Denominator);
}

分数的乘法只需要让两个分数的分子相乘,分母相乘,然后调用构造函数即可。

除法

template <typename OtherElementType> requires (requires (const ElementType& A, const OtherElementType& B) { A * B; })
[[nodiscard]] friend constexpr decltype(auto) operator/ (const Fraction& ThisFraction, const Fraction<OtherElementType>& OtherFraction)
{
    return Fraction(ThisFraction.Numerator * OtherFraction.Denominator, ThisFraction.Denominator * OtherFraction.Numerator);
}

本质上除法是分数 A 乘以 分数 B 的倒数,所以只需要让分子乘以分母即可。

倒数

void ToReciprocal()
{
    Swap(Numerator, Denominator);
}

实现一个分数的倒数,只需要将分子与分母交换即可。

T Temp(std::move(RHS));
RHS = std::move(LHS);
LHS = std::move(Temp);

Swap 的实现方式。

滚动至顶部