2017-05-11

constexpr ifの落とし穴

会社の同僚から、以下のようなコードが動かない、ネット上をググると解決策らしきものが見つかるがそれもいまいち納得できない、という相談を受けた。

template < typename T >
void f()
{
    if constexpr ( std::is_same<T, int>{} )
    {
        // Tがintのときのみ発動してほしい
        // 実際は常に発動する
        static_assert( false ) ;
    }
}

C++17にはconstexpr ifが追加された。これは条件付きコンパイルではない。条件付き実体化抑制だ。

constexpr ifは以下のように使う。

struct S
{
    int value() { return 42 ; }
} ;

template < typename T >
int to_int(T t)
{
    int value{} ;
    if constexpr ( std::is_same<T, S >{} )
    {
        value = t.value() ;
    }
    else if constexpr ( std::is_integral<T>{} )
    {
        value = static_cast<int>(t) ;
    }

    return value ;
}

int main()
{
    f(42) ;
    S s ;
    f( s ) ;
}

to_intは引数からint型の値を取り出すための関数だ。クラスSの場合、int型を取り出すためにメンバー関数valueを呼び出す特別な処理をする。

これをみると、constexpr ifは条件付きコンパイルのように思える。つまり、if文によって選択されるブランチのみをコンパイルする機能だ。しかし、それは間違いだ。

constexpr ifは選択されないブランチのテンプレートの実体化を抑制する機能だ。

テンプレートは、まず宣言を解釈され、その後、具体的なテンプレート実引数を与えられて実体化する。

template < typename T >
void f( T t )
{
    t.func() ;
} 

int main() { }

この例では、Tにint型を渡すとエラーとなる。なぜならばint型はメンバー関数funcを持っていないからだ。

しかし、この例はコンパイルエラーにならない。なぜならば、テンプレートfのテンプレート実引数にint型は渡されていないからだ。

テンプレートは、具体的な型をテンプレート実引数として渡され実体化して、初めてコードが正しいかどうか検証される。

というのは間違いで、テンプレートは宣言の時点でもコードが正しいかどうかを検証される。

template < typename T >
void f(T t)
{
    // エラー、名前gは宣言されていない
    g( 0 ) ;
}

template < typename T > void g( T ) { }


int main() { }

この例はエラーになる。なぜならば名前gは事前に宣言されていないからだ。C++は名前を使う際には、事前に明示的な宣言を求める言語だ。

しかし、以下のような例はエラーにならない。


template < typename T >
void f()
{
    // エラーではない
    T::g() ;
}

これはなぜかと言うと、名前T::gはテンプレート仮引数Tに依存しているからだ。テンプレート仮引数Tに依存している名前は、Tの具体的な型が与えられるまで、コードの合法性が検証できない。そのため、そのようなコードの検証はテンプレートの実体化まで遅延される。

まとめると、テンプレート仮引数に依存しないコードはテンプレートの宣言時に検証され、依存するコードはテンプレートの実体化時に検証されるということだ。

これを踏まえて、以下のコードが動かない理由を考えてみよう。


template < typename T >
void f(T t)
{
    if constexpr ( false )
    {
        // エラー、名前gは宣言されていない
        g() ; 
    }
}

名前gはテンプレート仮引数に依存していないため、テンプレートの宣言時に検証される。その結果、エラーとなる。

ここまでくれば、冒頭のコードが意図通りに動かない理由もわかるはずだ。

template < typename T >
void f()
{
    if constexpr ( std::is_same<T, int>{} )
    {
        // Tがintのときのみ発動してほしい
        // 実際は常に発動する
        static_assert( false ) ;
    }
}

static_assert(false)はテンプレート仮引数Tに依存していないため、テンプレートの宣言時に検証され、エラーとなる。

ではどうすればいいのか。たんにstatic_assertの中にconstexpr ifと同じ式をいれてやればよい。

template < typename T >
void f()
{
    static_assert( std::is_same<T, int>{} ) ;

    if constexpr ( std::is_same<T, int>{} )
    {
        // ...
    }
}

しかしこれは同じ式が重複してよろしくない。式を変えたいときは二箇所を同時に変更しなければならない。

式の重複を防ぐには、変数を使ってやればよい。

constexpr bool cond = expr ;
static_assert( cond ) ;
if constexpr ( cond ) { }

しかし、if文というのはネストさせたいものだ。

if constexpr ( E1 )
    if constexpr ( E2 )
        if constexpr ( E3 )
        { }
        else if constexpr ( E4 )
        {
            // static_assertしたい。
        }

このような複雑なネストしたif文に相当する式を書くのは面倒なので、constexpr ifの中に入れて、そのブランチが実体化されるときのみstatic_assertが働くようにしたい。

そのためにはどうすればいいかというと、static_assertのオペランドの式を依存式にすればよい。そうすればテンプレートの実体化まで評価を遅延させることができる。constexpr ifに囲まれていてテンプレートが実体化しないのであればstatic_assert(false)も発動しない。

using < typename ... >
constexpr bool false_v = false ;

template < typename T >
void f( T t )
{
    if constexpr ( std::is_same<T, int>{} )
    {
        static_assert( false_v<T> ) ;
    }
}

false_vというのは任意の型を取って常にfalseを返すテンプレートconstexpr変数だ。このテンプレートにテンプレート引数を与えてやれば依存させることができる。結果として、この関数fはTがint型のときのみstatic_assertを発動させる。

現在書いているC++17の参考書には、このような落とし穴も含めたC++17の新機能の解説を書いている。なるべく早く書き上げたい。そして、最近、C++の規格を学びすぎたために、こういうC++の初心者が陥りがちな落とし穴が何なのか自分ではわからなくなってしまっている。C++の落とし穴を教えてほしい。

3 comments:

齊藤 said...

これはテンプレートの特殊化でも理屈は同じですよね。

yumetodo said...

>テンプレートの実体化まで評価を遅延させることができる。

これってTwo Phase Name Look-upによるもの、と理解していいんでしょうか?

Anonymous said...

ペーパー出てますね。