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 ) ;
}
やっていることは、古典的なジャンプテーブルである。ただし、このジャンプテーブルは、コンパイル時に行われるという点が、少し変わっている。
訂正:誤って階乗としていたところを、べき乗に訂正。指数を正の整数に限定。