2010-11-17

gccにconstexprが実装された

GCC 4.6に、constexprが実装された。constexprについては、特に難しいことはない。単に、関数やクラスを、コンパイル時定数にできるというだけの話である。ともかく、せっかくなので使ってみる。この機能は、細々と解説するより、実際にコードを示したほうが分かりやすいであろう。

まずは、constexprな変数である。

int main()
{
    constexpr int a = 0 ; // OK

    int value = 0 ;
    constexpr int b = value ; // エラー
    const int c = value ; // OK
} 

constexpr指定された変数は、必ずコンパイル時定数になる。変数の初期化子は、定数式でなければならない。constとの違いは、constはコンパイル時定数でなくてもよいのである。constは、初期化子が定数式の場合、定数式になる。そうでない場合、単にconst修飾しているにすぎない。

次に、コンパイル時の数値計算をしてみよう。ここでは、べき乗の計算をすることにする。問題を簡単にするため、指数は正の整数とする。これを、constexpr関数で実装してみる。

constexpr double power( double x, unsigned int y )
{
    return y == 1 ? x : x * power( x, y - 1 ) ;
}

int main()
{
    // 定数式
    constexpr double a = power( 2, 32 ) ;
    // 定数式ではない
    double x = 2 ; unsigned int y = 32 ;
    double b = power( x, y ) ;
}

これは、非常に簡単なconstexprの例である。ご覧のように、constexpr関数は、通常の関数と全く同じように使える。実引数が定数式の場合のみ、結果も定数式となる。実引数が定数式でなければ、コンパイル時ではなく、実行時に、通常通りの関数が呼ばれる。これは、全く同じ内容のconstexpr版と通常版の関数を書く必要がないようにするための仕様である。

constexpr関数の本体は、必ず、{ return expression ; }の形でなければならない。つまり、constexpr関数の本体には、たったひとつのreturn文しか書けない。したがって、条件分岐にはcondition expressionを使い、ループには再帰を使わなければならない。もちろん、constexprを使おうなどと考えているプログラマは、すでに一流のメタプログラマであろうから、再帰には全く抵抗がない。したがって、これは問題にならないといえる。

しかし、constexpr関数の目的は、コンパイル時のべき乗や平方根、三角関数などといった、複雑な数値計算ではない。constexprの目的は、むしろ、もっと簡単な計算にある。重要なのは、constexpr関数の呼び出しは定数式なので、定数式を期待している場所に書けるということである。

さっそく、定数式が必要な場所で使ってみよう。

constexpr int twice( int value ) { return value * 2 ; }

template < int N > struct Foo { } ;

int main()
{
    constexpr int a = twice( 1 ) ;
    int b[ twice( 2 ) ] ;
    Foo< twice( 3 ) > foo ;
}

twiceとは、単に引数を二倍して返すだけの単純な関数である。ここでは、constexpr指定された変数の初期化子、配列の要素数、非型テンプレート実引数で、twice関数を使っている。これらはすべて、well-formedである。

constexpr関数は、数値絡みのプリプロセッサーによるマクロを、完全に置き換えることができる。プリプロセッサーは根本的に邪悪である。いやしくもC++プログラマたるものは、#defineと打つたびに恥じ入るべきである。思うに、プリプロセッサーには、中毒性があるのではないかと思う。現に、私の知るある奇人などは、寝る間も惜しんでプリプロセッサーメタプログラミングに打ち込んでいるそうである。私は何度も。プリプロセッサーは体に悪いからやめた方がいいと忠告したのだが、まるで聞き入れる様子がない。

話がそれた。constexprの真の威力は、まだこれからである。変数をconstexpr指定できるということは、すでに述べた。では、constexpr指定できる型とは何か。それは、「リテラル型」と呼ばれるものである。リテラル型には、三種類ある。まず、intやdouble、ポインターなどといった、スカラー型がある。スカラー型はリテラル型である。次に、リテラル型の配列は、リテラル型である。

ところで、C++には、言語組み込みの文法とユーザー定義のコードとを、なるべく一致させるという理念がある。だから、クラスは通常の変数と同じ文法で宣言できるし、演算子はオーバーロードできるようになっている。また、C++では、初期化リストも取り入れられた。とすれば、クラスも、コンパイル時定数になれてしかるべきではなかろうか。

リテラル型の最後の種類は、ある特別な条件を満たしたクラスである。具体的には、以下の条件を満たす必要がある。

  • trivialなコピーコンストラクターを持つこと
  • 非trivialなムーブコンストラクターを持たないこと
  • trivialなデストラクターをもつこと
  • trivialなデフォルトコンストラクターか、コピーでもムーブでもないconstexprコンストラクターを持つこと
  • 非staticデータメンバーと基本クラスは、すべてリテラル型であること

C++0xでは、コンストラクターをconstexpr指定できるのである。これにより、クラスがリテラル型になる。

具体例を示そう。以下はコンパイル時定数として使用可能なクラスである。

class Integer
{
private :
    int value ;

public :
    constexpr Integer() : value() { }
    constexpr Integer( int value ) : value(value) { }

    constexpr operator int() { return value ; }
} ;

int main()
{
    constexpr Integer size = 5 ; // コンパイル時定数

    int x[size] ; // Integer::operator int()が呼ばれる

    Integer object ; // 通常の実行時オブジェクト
    int y[object] ; // エラー
}

Integer型のオブジェクトsizeは、constexpr指定子が使われているので、コンパイル時定数である。また、int型への変換関数を持っている。したがって、int型の定数式を使えるところでは、どこでも使うことができる。一方、objectは、constexprが書かれていないので、通常のオブジェクトである。

もちろん、Integerにpublicな非staticデータメンバーを追加すれば、そのままクラス外部からでも定数式として使うことができるし、その他の非staticメンバー関数も、constexpr指定できる。

リテラル型のクラスは、夢が広がる機能である。

ところで、ひとつ夢のある話をしよう。constexpr関数の本体の中身は、たったひとつのreturn文でなければならないということは、すでに述べた。ということは、条件分岐をする場合、condition expressionと再帰を使わなければならないのであろうか。実は、もう一つ方法がある。リテラル型の配列型はリテラル型である。ということは、その配列は、constexpr関数の中から使えるのである。また、ポインターはリテラル型である。constexpr関数は、ポインター経由で呼び出すこともできる。すなわち、以下のようなコードが書けるのである。

namespace detail {
    // 演算モードの実装
    constexpr int plus_impl( int x, int y ) { return x + y ; }
    constexpr int minus_impl( int x, int y ) { return x - y ; }
    constexpr int mul_impl( int x, int y ) { return x * y ; }
    constexpr int div_impl( int x, int y ) { return x / y ; }
    // 演算モードのテーブル
    constexpr int (*table[])(int, int) = { &plus_impl, &minus_impl, &mul_impl, &div_impl } ;
}
// 演算モードのためのenum
enum struct Mode { plus, minus, mul, div } ;
// ユーザーへのAPI
constexpr int compute( Mode mode, int x, int y )
{
    return detail::table[ static_cast<int>(mode) ](x, y) ;
}

int main()
{
    constexpr int value = compute( Mode::minus, 1, 1 ) ;
}

やっていることは、古典的なジャンプテーブルである。ただし、このジャンプテーブルは、コンパイル時に行われるという点が、少し変わっている。

訂正:誤って階乗としていたところを、べき乗に訂正。指数を正の整数に限定。

1 comment:

Anonymous said...

もうこんな無茶をしなくていいのか…
http://d.hatena.ne.jp/w_o/20061008#p1