2013-11-08

emplaceとVariadic Templatesの世界一わかりやすい説明

以下はC++03のinsertの原理である。

template < typename T >
class container
{
private :
    alignas(T) char storage[ sizeof(T) ] ;
    T * ptr = nullptr ;
public :

    void insert( const T & x )
    {
        ptr = ::new(storage) T( x ) ;
    }
} ;

insertとは、何らかの方法で確保されたストレージ上に、オブジェクトを構築するのだ。placement newを使えば、任意の生のストレージ上に、オブジェクトを構築することができる。

push_backのようなメンバーは、insertのやや特殊なものである。

C++11では、ムーブセマンティクスを取り入れたため、insertにはrvalueリファレンスをとるものが追加された。

void container::insert( T && x )
{
    ptr = ::new(storage) T( std::move(x) ) ;
}

ムーブは、ポインターなどの所有権の移動や、コピーの概念のないリソース(ファイルポインタ―やスレッドなど)を扱うのに使うことができる。

オーバーロードが面倒な場合は、forwardingというテクニックを使うことができる。これはPerfect fowardingとも呼ばれている。

template < typename T >
void f( T && x )
{
    // xがlvalueリファレンスならコピー
    // xがrvalueリファレンスならムーブ
    T y = std::forward<T>(x) ;
}

これは、テンプレート仮引数に&&を指定して、関数の仮引数として使うと、実引数推定で、実引数がlvalueリファレンスの場合、Tの型がlvalueリファレンスになり、&&は、単に無視されるためである。

これは、コピーとムーブ以外にコードが変わらないテンプレートを、わざわざ重複させる必要がなくなるという点で素晴らしいのだが、xがlvalueリファレンスかrvalueリファレンスかで、コピーとムーブを切り替えないといけないという意味でもある。幸い、C++は十分にコンパイル時条件分岐が発達している。xのリファレンスの種類はコンパイル時にわかるので、そのような切り分けも、当然コンパイル時に行える。それが標準ライブラリforwardだ。

さて、emplaceとはなにか。emplaceとは、上記のコピーやムーブすらすっ飛ばし、placement newの時点で、直接オブジェクト構築のためのコンストラクターの実引数を指定するメンバーである。

それはどういうことなのか。また、なぜ必要なのか。以下のようなクラスを考える。

struct S
{
    S( int a, int b, int c )
    { /* 重たい処理 */ }

    // コピーコンストラクター
    S( S const & s )
    { /* 重たい処理 */}

    // ムーブコンストラクター
    S( S && s )
    { /* 重たい処理 */ }
} ;

このクラスは、何らかの理由で、int型の実引数を3個とるコンストラクターも、コピーやムーブのコンストラクターも、重たい処理を必要とする。このようなクラスは、ムーブすらしたくない。できるだけ構築回数を減らしたいのだ。

もし、コンテナーの中でストレージ上にオブジェクトを構築するplacement newに、int型の実引数を3つ渡せたら・・・、それがemplaceである。

template < typename T >
class container
{
private :
    alignas(T) char storage[ sizeof(T) ] ;
    T * ptr = nullptr ;
public :

    void emplace( int a1, int a2, int a3 )
    {
        ptr = ::new(storage) T( a1, a2, a3 ) ;
    }
} ;

これにより、まさにコンテナーが内部でオブジェクトをストレージに構築するその場に、構築の際の実引数を渡すことができる。

しかしまて、なにもコンストラクターの取る実引数が、int型3個とは限らない。やはりemplaceはテンプレート化しておかねばならないだろう。無論、perfect forwardingするムーブにもばっちり対応しなければならない。

template < typename T >
class container
{
private :
    alignas(T) char storage[ sizeof(T) ] ;
    T * ptr = nullptr ;
public :

    template < typename A1 >
    void emplace( A1 && a1 )
    {
        ptr = ::new(storage) T( std::forward<A1>(a1) ) ;
    }
} ;

よし・・・これでいい。いやまて、よくない。よく見ろ。これはTのコンストラクターの実引数が1個の場合にしか対応していないではないか。2個や3個の場合はどうするのだ。そもそも、0個の場合だってあるのだぞ。

おお、それもそうだ。何、心配御無用、C++にはちゃんとオーバーロードというものがあるのだ。まあ、見てるがいい。


template < typename T >
class container
{
private :
    alignas(T) char storage[ sizeof(T) ] ;
    T * ptr = nullptr ;
public :

    void emplace ()
    {
        ptr new(storage) T( ) ;
    }

    template < typename A1 >
    void emplace( A1 && a1 )
    {
        ptr = ::new(storage) T( std::forward<A1>(a1) ) ;
    }

    template < typename A1, typename A2 >
    void emplace( A1 && a1, A2 && a2 )
    {
        ptr = ::new(storage) T(
            std::forward<A1>(a1),
            std::forward<A2>(a2)
        ) ;
    }

    template < typename A1, typename A2, typename A3 >
    void emplace( A1 && a1, A2 && a2, A3 && a3 )
    {
        ptr = ::new(storage) T(
            std::forward<A1>(a1),
            std::forward<A2>(a2),
            std::forward<A3>(a3)
        ) ;
    }
} ;

どうだ。0個から3個までの、どんな型の実引数にも、パーフェクトな転送で対応したぞ。

「俺の書いたコンストラクターは実引数を5個取るんだけど?」

何、5個? よしわかった。今すぐ書いてやろう。まずはコピペをして・・・

もう読者も問題がわかったことだろう。自分の書いたソースコード内で関数を丸ごとコピペしてなにか付け加えるというのは、大抵の場合、機械的な繰り返しである。上のようなコードは、パターンさえ指定してやれば、機械的に展開して生成できる種類のコードである。人間が手作業で行うと、間違いが多い。

ちなみに、現在のC++規格で推奨されている、一つの関数における仮引数の数は、256個である。グッドラック!

もちろん、本物のプログラマーは運などあてにしない。本物のプログラマーは、機械的にできるような作業をしない。本物のプログラマーの労力は、本物のプログラミングに使われるべきなのだ。そもそも、このようなパターンのあるコードを生成するのは、まさにプログラミングの仕事である。

Variadic Templatesは、このようなパターンの繰り返しを展開できるようになる。言うよりコードを見たほうが早い。

template < typename ... Types >
void container::emplace( Types ... args )
{
    ptr = ::new(storage) T( std::forward<Types>(args)... ) ;
}

なんと、これだけで、0個以上、実装が許す仮引数以下の、すべての個数の仮引数に対応できるのだ。

ellipsisとよばれる...を使って宣言されたテンプレート仮引数と、ellipsisを使って宣言された関数の仮引数は、パラメーターパックと呼ばれる。このパラメーターパックは、様々な場所で、...を適用することにより、そのパターンを保ったまま展開できる。これをパック展開と呼ぶ。

template < typename ... Types >
void f( Types ... args ) ;

template < typename ... Types >
void g( Types ... args ) ;
{
    f( args... ) ;
    f( sizeof(Types)... ) ;
    f( (args + 1)... ) ;
}

さて、その他の細々としたものも埋めた、containerの全体をみてみよう。

template < typename T >
class container
{
public :
    using value_type = T ;
    using pointer = T * ;

private :
    alignas(value_type) char storage[ sizeof(value_type) ] ;
    pointer ptr = nullptr ;

public :

    template < typename ... Types >
    void emplace( Types ... args )
    {
        check_destruct() ;

        ptr = ::new(storage) value_type( std::forward<Types>(args)... ) ;
    }

    container() { }
    container( container const & c )
        : ptr( make_value(c) )
    { }
    container( container && c )
        : ptr( make_value( std::move(c) ) )
    { }

    container & operator = ( container const & c )
    {
        if ( this != &c )
        {
            check_destruct() ;
            ptr = make_value( c ) ;
        }

        return *this ;
    }

    container & operator = ( container && c )
    {
        if ( this != &c )
        {
            check_destruct() ;
            ptr = make_value( std::move(c) ) ;
        }

        return *this ;
    }

    ~container()
    {
        check_destruct() ;
    }

private :

    pointer make_value( container const & c )
    {
        return ::new(storage) value_type( *c.ptr ) ;
    }

    pointer make_value( container && c )
    {
        return ::new(storage) value_type( std::move(*c.ptr) ) ;
    }


    void check_destruct()
    {
        if ( ptr == nullptr ) { return ; }

        ptr->~value_type() ;
        ptr = nullptr ;
    }
} ;

もちろん、やりだすときりがない。イテレーター、swapなどなど。

とにかく使ってみよう。本当に使えるのだろうか。ところで、一体どうやって、様々な実引数を取るクラスを書くのか。もちろんVariadic Templatesだ。

struct S
{
    template < typename ... Types >
    S( Types ... ) { }
} ;

int main()
{
    container<S> c ;

    c.emplace() ;
    c.emplace( 1 ) ;
    c.emplace( 1, 2, 3, 4, 5 ) ;
    c.emplace( 3.14, 'a', "hello" ) ;
}

さて、本当に呼び出せているのだろうか。念の為に、クラスSのコンストラクターに、古典的なprintfデバッグを仕掛けてみてはどうだろうか。

struct S
{
    template < typename ... Types >
    S( Types ... args)
    {
        // すべての実引数を標準出力に出力する
        print( args... ) ;
        std::cout << '\n' ;
    }
}

この関数テンプレートprintをどうやって書けばいいのか。これは、Variadic Templatesを再帰的に使えばよい。

struct S
{
    template < typename ... Types >
    S( Types ... args )
    {
        print( args... ) ;
        std::cout << "\n" ;
    }

    void print() { }

    template < typename T, typename ... Types >
    void print( T const & head, Types const & ... tail )
    {
        std::cout << head ;
        if ( sizeof...(tail) != 0)
        { std::cout << " , " ; }

        print( tail... ) ;
    }
} ;

sizeof...をパラメーターパックに使うことで、そのパラメーターパックにいくつのパラメーターが入っているのかを、コンパイル時に調べることができる。

さらに詳しくC++を学びたい場合は、私の書いた自由な参考書が役に立つだろう。

GitHub: EzoeRyou/cpp-book

GitHubからzipでダウンロード

GitHub Pagesでの閲覧:C++11の文法と機能

本の虫: C++11参考書の公開:C++11の文法と機能

No comments: