C++ 编译时有理数算术 深度解析(c++有理数类)

C++ 编译时有理数算术 深度解析(c++有理数类)

编码文章call10242025-07-21 11:51:585A+A-

C++11 引入的 <ratio> 头文件提供了一套用于在编译时表示和操作有理数的工具。这使得开发者可以在编译期间执行精确的分数运算,并将结果作为类型的一部分,用于模板元编程、静态断言、单位转换等场景。

核心组件

1. std::ratio<Num, Denom>

std::ratio 是一个类模板,用于表示一个有理数。它接受两个 std::intmax_t 类型的非类型模板参数:

  • Num: 分子 (Numerator)
  • Denom: 分母 (Denominator),默认为 1。
 template <std::intmax_t Num, std::intmax_t Denom = 1>
 struct ratio {
     static constexpr std::intmax_t num = /* ... */; // 约分后的分子
     static constexpr std::intmax_t den = /* ... */; // 约分后的分母
     typedef ratio<num, den> type; // 自身约分后的类型
     // C++17: double to_double() const noexcept; // (非静态成员函数,但通常通过静态访问)
 };

重要特性:

  • 编译时常量: numdenstatic constexpr 成员,表示约分后的分子和分母。这意味着它们的值在编译时就已确定。
  • 自动约分: std::ratio 会自动将分数约分为最简形式。例如,std::ratio<4, 8> 实际上表示 std::ratio<1, 2>
  • 分母非零: 标准要求 Denom 不能为零。如果 Denom 为零,程序是病构的 (ill-formed)。
  • 符号: 分数的符号由分子承载,分母始终为正。例如 std::ratio<1, -2> 会被规范化为 std::ratio<-1, 2>
  • type 成员: type 是一个类型别名,指向 std::ratio 本身约分后的形式。
  • to_double() (C++17): 虽然定义为非静态成员函数,但通常通过 std::ratio<N,D>::to_double() 这样的静态方式(如果编译器支持)或通过实例来调用,用于将有理数转换为 double 类型。在C++17之前,通常需要手动计算 static_cast<double>(num) / den

示例:

 #include <iostream>
 #include <ratio>
 
 int main() {
     typedef std::ratio<1, 2> one_half;
     std::cout << "One half: " << one_half::num << "/" << one_half::den << std::endl;
 
     typedef std::ratio<10, 20> ten_twentieths; // 自动约分为 1/2
     std::cout << "Ten twentieths: " << ten_twentieths::num << "/" << ten_twentieths::den << std::endl;
 
     typedef std::ratio<2, -3> two_minus_thirds; // 规范化为 -2/3
     std::cout << "Two minus thirds: " << two_minus_thirds::num << "/" << two_minus_thirds::den << std::endl;
 
     typedef std::ratio<0> zero; // Denom 默认为 1, 表示 0/1
     std::cout << "Zero: " << zero::num << "/" << zero::den << std::endl;
 
     // 检查类型是否相同
     static_assert(std::is_same<one_half, ten_twentieths::type>::value, "Ratios should be the same type after normalization");
 
     // 转换为 double (C++17 风格,如果支持)
     // 或者手动转换: static_cast<double>(one_half::num) / one_half::den
     #if __cplusplus >= 201703L
         std::cout << "One half as double: " << one_half::to_double() << std::endl;
     #else
         std::cout << "One half as double: " << static_cast<double>(one_half::num) / one_half::den << std::endl;
     #endif
 
     return 0;
 }

2. 编译时有理数算术

<ratio> 库提供了一系列类模板,用于对 std::ratio 对象执行编译时的算术运算。这些运算的结果也是一个 std::ratio 类型。

  • std::ratio_add<R1, R2>: 计算 R1 + R2
  • std::ratio_subtract<R1, R2>: 计算 R1 - R2
  • std::ratio_multiply<R1, R2>: 计算 R1 * R2
  • std::ratio_divide<R1, R2>: 计算 R1 / R2

这些模板的结果通过其名为 type 的成员类型别名提供,该别名指向表示计算结果的 std::ratio

示例:

 #include <iostream>
 #include <ratio>
 
 int main() {
     typedef std::ratio<1, 2> r1;
     typedef std::ratio<1, 3> r2;
 
     // 加法: 1/2 + 1/3 = 3/6 + 2/6 = 5/6
     typedef std::ratio_add<r1, r2> sum;
     std::cout << "1/2 + 1/3 = " << sum::num << "/" << sum::den << std::endl;
     static_assert(sum::num == 5 && sum::den == 6, "Addition error");
 
     // 减法: 1/2 - 1/3 = 3/6 - 2/6 = 1/6
     typedef std::ratio_subtract<r1, r2> diff;
     std::cout << "1/2 - 1/3 = " << diff::num << "/" << diff::den << std::endl;
     static_assert(diff::num == 1 && diff::den == 6, "Subtraction error");
 
     // 乘法: 1/2 * 1/3 = 1/6
     typedef std::ratio_multiply<r1, r2> prod;
     std::cout << "1/2 * 1/3 = " << prod::num << "/" << prod::den << std::endl;
     static_assert(prod::num == 1 && prod::den == 6, "Multiplication error");
 
     // 除法: (1/2) / (1/3) = 1/2 * 3/1 = 3/2
     typedef std::ratio_divide<r1, r2> quot;
     std::cout << "(1/2) / (1/3) = " << quot::num << "/" << quot::den << std::endl;
     static_assert(quot::num == 3 && quot::den == 2, "Division error");
 
     // 链式运算: (1/2 + 1/3) * 3 = 5/6 * 3/1 = 15/6 = 5/2
     typedef std::ratio_multiply<std::ratio_add<r1, r2>::type, std::ratio<3,1>> complex_op;
     std::cout << "(1/2 + 1/3) * 3 = " << complex_op::num << "/" << complex_op::den << std::endl;
     static_assert(complex_op::num == 5 && complex_op::den == 2, "Complex operation error");
 
     return 0;
 }

溢出处理: 如果算术运算的结果超出了 std::intmax_t 的表示范围,程序是病构的。

3. 编译时有理数比较

<ratio> 库还提供了用于在编译时比较两个 std::ratio 对象的类模板。

  • std::ratio_equal<R1, R2>: 判断 R1 == R2
  • std::ratio_not_equal<R1, R2>: 判断 R1 != R2
  • std::ratio_less<R1, R2>: 判断 R1 < R2
  • std::ratio_less_equal<R1, R2>: 判断 R1 <= R2
  • std::ratio_greater<R1, R2>: 判断 R1 > R2
  • std::ratio_greater_equal<R1, R2>: 判断 R1 >= R2

这些模板都派生自 std::integral_constant<bool, value>,其中 value 是比较的结果。因此,它们有一个 static constexpr bool value 成员,以及一个到 bool 的转换运算符。

示例:

 #include <iostream>
 #include <ratio>
 
 int main() {
     typedef std::ratio<1, 2> r1;
     typedef std::ratio<2, 4> r2; // 等于 1/2
     typedef std::ratio<1, 3> r3;
 
     static_assert(std::ratio_equal<r1, r2>::value, "r1 should be equal to r2");
     std::cout << "1/2 == 2/4: " << std::boolalpha << std::ratio_equal<r1, r2>::value << std::endl;
 
     static_assert(std::ratio_not_equal<r1, r3>::value, "r1 should not be equal to r3");
     std::cout << "1/2 != 1/3: " << std::ratio_not_equal<r1, r3>::value << std::endl;
 
     static_assert(std::ratio_less<r3, r1>::value, "1/3 should be less than 1/2");
     std::cout << "1/3 < 1/2: " << std::ratio_less<r3, r1>::value << std::endl;
 
     static_assert(std::ratio_greater<r1, r3>::value, "1/2 should be greater than 1/3");
     std::cout << "1/2 > 1/3: " << std::ratio_greater<r1, r3>::value << std::endl;
 
     static_assert(std::ratio_less_equal<r1, r2>::value, "1/2 should be less than or equal to 2/4");
     std::cout << "1/2 <= 2/4: " << std::ratio_less_equal<r1, r2>::value << std::endl;
 
     static_assert(std::ratio_greater_equal<r1, r3>::value, "1/2 should be greater than or equal to 1/3");
     std::cout << "1/2 >= 1/3: " << std::ratio_greater_equal<r1, r3>::value << std::endl;
 
     return 0;
 }

4. SI 单位前缀 (SI Prefixes)

<ratio> 库预定义了一系列 std::ratio 类型,用于表示标准的SI单位前缀。这在与 <chrono> 库(用于时间和持续期)结合使用时特别有用。

类型名

值 (Num/Denom)

前缀

符号

std::atto

std::ratio<1, 10^18>

atto

a

std::femto

std::ratio<1, 10^15>

femto

f

std::pico

std::ratio<1, 10^12>

pico

p

std::nano

std::ratio<1, 10^9>

nano

n

std::micro

std::ratio<1, 10^6>

micro

u

std::milli

std::ratio<1, 1000>

milli

m

std::centi

std::ratio<1, 100>

centi

c

std::deci

std::ratio<1, 10>

deci

d

std::deca

std::ratio<10, 1>

deka

da

std::hecto

std::ratio<100, 1>

hecto

h

std::kilo

std::ratio<1000, 1>

kilo

k

std::mega

std::ratio<10^6, 1>

mega

M

std::giga

std::ratio<10^9, 1>

giga

G

std::tera

std::ratio<10^12, 1>

tera

T

std::peta

std::ratio<10^15, 1>

peta

P

std::exa

std::ratio<10^18, 1>

exa

E

示例:与 <chrono> 结合

std::chrono::duration 模板的第二个参数就是一个 std::ratio 类型,表示时间单位相对于秒的比例。

 #include <iostream>
 #include <ratio>
 #include <chrono>
 
 int main() {
     // std::chrono::seconds uses std::ratio<1, 1> (implicitly)
     std::chrono::seconds s(5);
 
     // std::chrono::milliseconds uses std::milli (which is std::ratio<1, 1000>)
     std::chrono::milliseconds ms(5000);
 
     // std::chrono::microseconds uses std::micro (std::ratio<1, 1000000>)
     std::chrono::microseconds us(123);
 
     // std::chrono::nanoseconds uses std::nano (std::ratio<1, 1000000000>)
     std::chrono::nanoseconds ns(456);
 
     // 自定义 duration 类型,例如表示 1/60 秒 (用于帧率)
     typedef std::chrono::duration<double, std::ratio<1, 60>> frame_period;
     frame_period one_frame(1.0);
 
     std::cout << "5 seconds is " << ms.count() << " milliseconds." << std::endl;
 
     if (s == ms) {
         std::cout << "5 seconds is equal to 5000 milliseconds." << std::endl;
     }
 
     // 转换:1秒有多少纳秒?
     // std::nano::den 是 10^9, std::nano::num 是 1
     // 1 second = (std::nano::den / std::nano::num) nanoseconds
     std::cout << "1 second = " << std::nano::den << " nanoseconds." << std::endl;
     // 1 kilometer = std::kilo::num meters
     std::cout << "1 kilometer = " << std::kilo::num << " meters." << std::endl;
 
     return 0;
 }

应用场景

  • 类型安全的单位转换: 如 <chrono> 库中用于表示不同时间单位。
  • 模板元编程: 在编译时进行复杂的数值计算和决策。
  • 静态断言: 验证编译时的常量关系。
     template <typename LengthUnit, typename TimeUnitPrice>
     struct SpeedUnit {
         // Speed = Length / Time
         using type = std::ratio_divide<LengthUnit, TimeUnitPrice>;
         static_assert(type::den != 0, "Time unit cannot be zero for speed");
     };
     typedef SpeedUnit<std::kilo, std::ratio<3600>> km_per_hour; // km / (3600s)
  • 配置常量: 定义精确的比例或因子,这些因子在编译时固定。
  • 物理或工程计算: 当需要高精度且在编译时确定的比例时。

注意事项

  • 编译时间: 大量复杂的 std::ratio 运算可能会增加编译时间。
  • 整数类型限制: 分子和分母基于 std::intmax_t,这意味着它们有其表示范围的限制。非常大或非常小的分数可能无法精确表示或导致溢出(编译错误)。
  • 调试: 编译时计算的调试可能比运行时更具挑战性,通常依赖于编译器错误信息和 static_assert

总结

std::ratio 库为C++提供了强大的编译时有理数算术能力。它通过类型系统确保了运算的精确性,避免了浮点数带来的精度问题,并在模板元编程和类型安全的单位系统中扮演着重要角色。虽然其主要应用场景是在编译期,但它为构建更安全、更精确的数值相关代码提供了坚实的基础。

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4