以下は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
You can use some HTML elements, such as <b>, <i>, <a>, also, some characters need to be entity referenced such as <, > and & Your comment may need to be confirmed by blog author. Your comment will be published under GFDL 1.3 or later license with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.