2013-04-14

最新のconstexpr

constexprは何度も解説しているが、だいぶ昔のドラフトを元にしていたので、正式な規格とはずれている。今書いている参考書はもうしばらく時間がかかるので、とりあえずその間をつなぐために、constexprについて解説する。

constexpr変数

constexpr指定子をつけてリテラル型の変数を宣言すると、その変数はコンパイル時定数になる。変数の初期化式はコンパイル時定数でなければならない。

void f()
{
    constexpr int x = 10 ; // OK、コンパイル時定数

    int r = 0 ;
    constexpr int error = r ; // エラー
}

constexpr指定子をつけて関数を宣言すると、constexpr関数になる。constexpr関数は、コンパイル時に評価される。ただし、constexpr関数にはとても厳しい制限がある。

まず、仮引数の型と戻り値の型は、リテラル型でなければならない。virtual関数であってはならない。関数の本体は、=defaultか、=deleteか、あるいは複合文で、しかもその中身が極端に制限される。

constexpr int f()
{
    ; // null文
    static_assert( true, "..." ) ; // static_assert宣言
    typedef int type1 ; // typedef宣言
    using type2 = int ; // エイリアス宣言
    using std::vector ; // using宣言
    using namespace std ; // using directive

    return 0 ; // 唯一のreturn文
}

これ以外の文を書くことはできない。これでもだいぶ制限が緩和されたのだ。昔のドラフトでは、return文ひとつの他には何も書けなかった。

もちろん、変数の宣言なんてできない。(x+y)(x+y)を返すconstexpr関数は以下のようになる。

// エラー
constexpr int error( int x, int y )
{
    constexpr int temp = x + y ;
    return temp * temp ;
}

// OK
constexpr int ok( int x, int y )
{
    return (x + y) * (x + y) ;
}

if文も使えないので、条件分岐は条件演算子や論理和や論理積を使うことになる。|x + y|を返すconstexpr関数は以下のようになる。

// エラー
constexpr int error( int x, int y )
{
    constexpr int temp = x + y ;
    if ( temp < 0 )
        return -temp ;
    else
        return temp ;
}

// OK
constexpr int ok( int x, int y )
{
    return (x + y) < 0 ? -(x + y) : (x + y) ;
}

もちろん、絶対値の計算を別のconstexpr関数で実装して丸投げすることは可能だ。

ループが使いたければ再帰だ。aのb乗(a,bは正の整数)を計算するconstexpr関数は以下のようになる。

// エラー
constexpr unsigned long error( unsigned long a, unsigned long b )
{
    unsigned long result = 1 ;

    for ( ; b != 0 ; --b )
    {
        result *= a ;
    }

    return result ;
}

// OK
constexpr unsigned long ok( unsigned long a, unsigned long b )
{
    return b == 0 ? 1 : a * ok( a, b-1 ) ;
}

この問題に関しては、中には再帰の方が分かりやすいと考える人もいるかもしれないが、あらゆるループを再帰で書くのは面倒だ。

現在、constexpr関数の制限を大幅に緩和して、普通に変数や任意の文を使えるようにすることが議論されている。

ちなみに、constexpr関数に対してコンパイル時定数以外を渡すと、実行時評価される。

constexpr int f( int x )
{
    return x ;
}
void g( int x )
{
    int result = f( x ) ; // 実行時評価
}

constexpr指定子をコンストラクターに指定すると、constexprコンストラクターとなる。constexprコンストラクターにはとても厳しい制限がある。constexpr関数の制限と似ているが、こちらはreturn文すら使えない。さらに、クラスのサブオブジェクトをすべて初期化しなければならない。この他にも厳しい制限が多数あるが、見事すべての制限を満たした暁には、なんとconstexprコンストラクターを持つクラスは、リテラル型になるのだ。

// クラスXはリテラル型
struct X
{
    int x ;
    int y ;

    constexpr X( int x = 0, int y = 0 )
        : x(x), y(y)
    { }
} ;

リテラル型ということは当然、constexpr変数の型や、constexpr関数の仮引数や戻り値の型として使えるという事だ。

struct Integer
{
    int member ;
    constexpr Integer( int value = 0 )
        : member( value )
    { }
    constexpr operator int() { return member ; }
} ;

constexpr Integer f( Integer i )
{
    return i + 1 ;
}


int main()
{
    constexpr Integer i = 10 ;
    constexpr Integer result = f( i ) ;

    std::cout << result << std::endl ;
}

つまり、constexprコンストラクターを持つクラスはコンパイル時定数として使えることを意味する。

とまあ、constexprは規格にして4ページ程度の小さな機能である。

No comments: