読者です 読者をやめる 読者になる 読者になる

ボレロ村上 - ENiyGmaA Code

中3女子です。

constexpr を使うべき5の理由 - その5


「constexpr を使うべき5の理由」ひとまず最終回です。
コンパイル時に計算ができると聞いて、誰もがまず思い浮かべる用途は、おそらく、実行時に必要となる値をあらかじめ計算しておくというものでしょう。

5.必要なものをなるべく早く用意しておくために


何度も繰り返し同じような計算を行い、しかも実行速度が要求されるような場合(例えばゲームなど)では、最適化のために
自明な計算をあらかじめ行っておいて結果をテーブルに保持しておくことがしばしばあると思います。


非常にシンプルな例として、度数法で [0 .. 90) の範囲の正弦値を保持するテーブルを考えてみます。

#include <cmath>

double degree_sin_table[90];
void init_table() {
    const double pi = 3.141592653589793238462643383279502884197;
    for (int i = 0; i < 90; ++i) {
        degree_sin_table[i] = std::sin(i * pi / 180);
    }
}

int main() {
    init_table();
    // ...
}

これは見てのとおり C言語的なやり方で実装したコードである。
init_table を実行時に一度だけ呼び出しておく必要があり、非常に煩わしい。

#include <cmath>
#include <array>

std::array<double, 90> calc_table() {
    const double pi = 3.141592653589793238462643383279502884197;
    std::array<double, 90> result;
    for (int i = 0; i < 90; ++i) {
        result[i] = std::sin(i * pi / 180);
    }
    return result;
}
const std::array<double, 90> degree_sin_table = calc_table();

C++ 的に考えれば、degree_sin_table を定数にして、ユーザコードで初期化を呼び出す必要を無くすことはできる。
あるいは degree_sin_table をシングルトンにする実装なども考えられる。


では、この degree_sin_table が初期化されるのはいったいいつの時点だろうか。
通常のグローバル変数(定数)であれば、アプリケーションの起動時である。
シングルトンであれば、最初に値を要求した時点で計算が行われる実装もありうる。


いずれにせよ実際に計算されるのは実行時である。
もちろんこの程度の計算なら速度が問題になることもないだろうが、場合によってはもっと多くの計算を必要とするものもあるだろう。
グローバル変数の場合、初期化順序が問題になるケースもあるかもしれない。


もし『constexpr を使うべき5の理由』を読んできたあなたならば、きっとこう思ったに違いない。


よろしい、ならば constexpr だ。

#include <sprout/array.hpp>
#include <sprout/range/adaptor.hpp>

constexpr sprout::array<double, 90> degree_sin_table
    = sprout::adaptors::sinusoidal(1. / 360/*周波数*/)
    | sprout::adaptors::copied
    ;

なにやら怪しげな書き方に思えるかもしれないが、ようは constexpr Rangeアダプタでテーブルを生成している。
「Rangeアダプタってなにそれ」という方は "Pstade.Oven" や "Boost.Range" で検索されたし。


ともあれ、ライブラリ化されたものを使えば、constexpr で簡潔に処理を書くことも、様々なパラダイムを扱うこともできることがお分かりになるだろう。
当然ながら、先に挙げた非 constexpr なコードを constexpr に書き直してベタ書きで計算することもできるので、もし興味がある方は試して戴きたい。


ところで、こうしたテーブルを定義する古典的な方法として、何かツールで生成したデータを書き出した CSV ファイルをインクルードするというものがある。

constexpr double degree_sin_table[] = {
#   include "sin.csv"
};

わざわざ constexpr で計算しなくても、これでよいのではないか?


それに対しては、逆にこう質問したい。
・そのツールは、条件分岐や再帰を含む複雑な計算あるいはエラーによるブレークを、必要ならば行えるか?
・そのツールは、C++プリプロセッサ型推論のような他の独立した機能との連携を、必要ならば行えるか?
・そのツールは、出力した値を別の計算に再利用するために用いることが、必要ならば行えるか?


constexpr でデータを生成することの利点の一つは、必要ならば C++ の(少なくともプリプロセスとコンパイル時の)すべての機能が使えることだ。
それは、例えば C++ でパーサを作成するとき yacc+lex よりも Boost.Spirit.Qi を使うのを好むプログラマが存在する理由と同じである。


かの大数学者は言った。微分のことは微分でせよ」、と。
だからあなたも胸をはってこう言うべきである。


C++ のことは C++ でせよ」


そしてコンパイル時にできることはコンパイル時にしておきましょう。