(2019.6)
テンプレートを使う
C++ では昔から、テンプレートを使えば、コンパイル時計算を行えた。これは Template Meta-Programming (TMP) と呼ばれる。
階乗をコンパイル時計算してみよう (C++11). gcc だと -std=c++11 オプション.
C++
- #include <stdio.h>
-
-
-
- template< unsigned n >
- struct factorial {
-
-
-
-
-
-
-
-
-
-
- static constexpr unsigned value = n * factorial<n - 1>::value;
- };
-
-
- template<>
- struct factorial<0> {
- static constexpr unsigned value = 1;
- };
-
-
- int main() {
- printf("f(10) = %d\n", factorial<10>::value);
- return 0;
- }
確認のためアセンブラを出力させると, 結果が即値として埋め込まれている。
movl $3628800, %edx
leaq .LC0(%rip), %rcx
call printf
constexpr
新たに導入された constexpr
キーワードを使えば、はるかに簡単に書ける。
constexpr
キーワードのようなヒントがなくても, 原理的にはコンパイル時に読みきれれば, コンパイル時計算は加能だが、ソースコード全体に渡ってそのような判定をするのは現実的でない。constexpr
がない箇所では, 最適化によって自動的にコンパイル時計算を行うようにはなっていないようだ。(gcc 7.3)
次のように、普通に関数を書くだけで, コンパイル時計算になる。
C++
- #include <stdio.h>
-
- constexpr unsigned factorial(unsigned N)
- {
- return N == 0 ? 1 : N * factorial(N - 1);
- }
-
- int main() {
- printf("f(10) = %d\n", factorial(10));
- return 0;
- }
即値になっている。
leaq .LC0(%rip), %rcx
movl $3628800, %edx
call printf
constexpr
関数は、それだけではコンパイル時計算を強制できない。実引数として定数式でない値を渡せるし、その場合は、実行時計算になる。
constexpr
関数の throw
式は, さすがにコンパイルエラーになる。
C++
- #include <stdio.h>
- #include <stdlib.h>
-
- constexpr unsigned factorial(unsigned N)
- {
- printf("1\n");
-
- return N == 0 ? 1 : N * factorial(N - 1);
- }
-
- int main( int argc, char* argv[] ) {
- unsigned t = factorial(atoi(argv[1]));
-
- printf("f(10) = %d\n", t);
- return 0;
- }
constexpr
関数内で、副作用のある式も書ける。それだけではコンパイルエラーにならない。
コンパイル時計算が強制されて、かつ実行経路上の文に副作用があれば、コンパイルエラーになる。コンパイル時計算が強制されない場合は、実行時計算になる。
コンパイル時計算を強制
コンパイル時計算を強制するには、関数の戻り値をconstexpr
変数に代入する。
コンパイル時計算が強制されると、矛盾が生じるため、先ほどの関数内のprintf()
もコンパイルエラーになる。
値の検査は、assert()
を使うのが簡単。条件を満たさないときは、エラーメッセージの表示という副作用が生じるので、コンパイルエラーになる。
このコードは、C++11 ではコンパイルできない (C++14が必要) が、C++11 のことはもう忘れていい。
C++
- #include <stdio.h>
- #include <assert.h>
-
-
- constexpr int sum(int n) {
- assert(n >= 0);
-
- int x = 0;
- for (int i = 0; i <= n; ++i) {
- x += i;
- }
- return x;
- }
-
- int main () {
- constexpr int x = sum(100);
-
-
- printf("%d\n", x);
- return 0;
- }
ちょっと複雑な計算
たらい回し関数 (竹内関数) を試してみる。C言語による最新アルゴリズム事典 (1991年) では「特に用途はない.」と評されている。
コードの通りに実行すると、かなり時間が掛かる。
C++
- #include <stdio.h>
-
- constexpr int tarai(int x, int y, int z) {
- if (x <= y) return y;
- return tarai(tarai(x - 1, y, z),
- tarai(y - 1, z, x),
- tarai(z - 1, x, y));
- }
-
- int main() {
-
- constexpr int t = tarai(192, 96, 0);
- printf("%d\n", t);
- return 0;
- }
はい, 即値.
leaq .LC0(%rip), %rcx
movl $192, %edx
call printf
もっともっと複雑なものでも、コンパイル計算を行える、らしい。
クラスインスタンス
クラス/構造体のインスタンスもconstexpr定数にできる。コンストラクタを constexpr
修飾しなければならない。
C++
- #include <stdio.h>
- #include <math.h>
-
-
- constexpr int g() {
-
- return 1;
- }
-
- struct X {
- int y;
-
-
-
-
-
- constexpr X(): y(pow(5, 3)) {
- g();
- }
- };
-
- int main() {
- constexpr X x = X();
- return 0;
- }