前言
在引擎库中,有时候可能会需要准确描述精度,如果用平常的 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
整除,所以 2
是 12
的一个约数,而最大公约数是两个整数共有约数最大的一个。
例如 12
和 16
的最大公约数是 4
,因为 12
与 16
可以同时被整除的最大数就是 4
。
为了计算最大公约数,可以使用 GCD 函数,具体实现方式如下
int Gcd(int A, int B) {
return B == 0 ? A : Gcd(B, A % B);
}
这是一个欧几里得算法,如果 B
是 A
的约数,那么 A
和 B
的最大公约数就是 B
(因为比 B
再大的数无法被 B
整除。),
如果不是,那么 A
和 B
的最大公约数与 B
与 A % 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();
}
可以看到已经自动化简了。
减法
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 的实现方式。