C++にはコンパイル時の有理数計算ライブラリがある。個人的な理解では、これは<chrono>
のために入ったライブラリで、<chrono>
がduration
を綺麗で速い(使いやすいとは言っていない)やり方で実装するためのものだ。コンパイル時に比率を計算できるので、実行時にかかるオーバーヘッドはせいぜい整数か浮動小数点数の掛け算くらいしか残らないことになる。
ご存知の通り、浮動小数点数は我々の知る実数とは少しだけ異なる。有限のメモリ領域を使ってできる限り広い範囲の数を表現しようとしてはいるが、もちろん完璧ではない。特に、我々は指が10本なので単位系を10の倍数ベースに作っており、そして悲しいことに0.1は2進数では循環少数になる。なので計算途中に丸めが入ることになる。これを避けるためには、整数同士の比で計算するしかない。整数は(実行時に伸びる多倍長整数を使えば)事実上無限の長さを扱えるので、有理数で計算している限り丸め誤差は入る余地がない。まあ、コンパイル時に計算する場合は固定長でなければならないのだが。コンパイル時多倍長演算を実装するという手は置いておいて。
なのでstd::ratio
は2つのstd::intmax_t
を取る。それぞれが分子と分母に相当する。そして演算もサポートされていて、ratio_add
、ratio_subtract
、ratio_multiply
、ratio_divide
が用意されている。これらを使えば、約分まで済んだstd::ratio
が得られる。また、比較も用意されていて、ratio_equal
その他がそれぞれ定義されている。
また、主目的が単位換算なので、SI接頭辞(std::kilo
やstd::milli
など)も定義されている。
さて、単位には非常に大きな数値が登場するものがあったりする。例えば、分子量は6.02e+23の大きさがあり、原子1個あたりの重さは例えば12 / 6.02e+23グラムになったりする。ところで、log10(264)の値は20に届かない。なのでおそらくこの単位はほとんどの環境でオーバーフローを引き起こしてしまい、上記の枠組みに乗らなくなってしまうだろう。
となると、この際丸め誤差には目をつぶって、コンパイル時に浮動小数点演算を行うしかない。幸いなことにC++11以降ではconstexpr
があるので、浮動小数点演算を行うことが可能だ。すると、既存のstd::ratio
と相互運用可能なfratio
とでも言うべきものを作る必要がある。
面倒なことにC++のnon-type template argumentは浮動小数点数を持てないので、static constexpr double value = /**/;
のようにする必要がある。
struct mole { static constexpr double value = 6.02e22; }; template<typename Numer, typename Denom> struct fratio { static constexpr double num = Numer::value; static constexpr double den = Denom::value; static constexpr double value = num / den; };
しかしながらstd::ratio
はstd::ratio::value
を持っていないので、共通で使うには::value
を使ってはいけない。とはいえfratio
の中で::value
を使わざるを得ない以上(再帰的に定義できた方が便利だ)、共通のインターフェースを用意しておく必要がある。
template<typename T> struct value_of { static constexpr double value = T::value; }; // std::ratioへの部分特殊化 template<std::intmax_t N, std::intmax_t D> struct value_of<std::intmax_t<N, D>> { // 有理数をdoubleに変換する static constexpr double value = static_cast<double>(N) / D; };
これを噛ませれば、std::ratio
を共通で使っていくことが可能になる。
正確な数値という利便性を捨ててしまうことになるが、多少の丸め誤差が問題ない場合はこれでことが足りる。もしどうしても正確な数値が必要なら、std::intmax_t
のペアもしくは可変長引数を取ることによって多倍長を実現しても構わないのではあるが、使うのがとても難しくなってしまうので、難しいところだ。