C++ 编译时有理数算术 深度解析(c++有理数类)
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; // (非静态成员函数,但通常通过静态访问)
};
重要特性:
- 编译时常量: num 和 den 是 static 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++提供了强大的编译时有理数算术能力。它通过类型系统确保了运算的精确性,避免了浮点数带来的精度问题,并在模板元编程和类型安全的单位系统中扮演着重要角色。虽然其主要应用场景是在编译期,但它为构建更安全、更精确的数值相关代码提供了坚实的基础。