以下は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 Pagesでの閲覧:C++11の文法と機能
No comments:
Post a Comment