2008-08-16

N2680について

コンテナにとてつもなく重いオブジェクトを入れたいとする。そのクラスは、move可能ではなく、コピーもできないか、とてつもなく重いときている。これを従来のコンテナに入れようとすると、問題がある。push_backなどのオブジェクトを挿入するメンバ関数は、オブジェクトを必要とするからだ。だから、まずオブジェクトを構築してpush_backに渡し、push_backはそのオブジェクトをコピーしてメモリ上にオブジェクトを構築する。先ほども言ったように、このオブジェクトのコピーは、構築と変わらないほどコストがかかる。なぜ二回も構築しなければならないのか。一回構築すれば、それで十分なはずだが、現在のコンテナはそれを可能にするインターフェースが無い。

そのクラスの中には、一部は動的に確保するmove可能なメンバもあるかもしれない。しかしmove可能ではないメンバ変数もたくさんある。一体どうすれば良いのか。例えば次のように実装してみよう。

//初期化用のためだけにある糞クラス
class Foo_data
{
public :
    Foo_data(int a, int b, int c) {/*時間のかかる処理*/}
} ;

class Foo
{
public :
Foo( ){/*何もしない*/}// トリビアルなコンストラクタ

    //実行のとても時間のかかるコンストラクタ
    Foo( int a, int b, int c ){/*時間のかかる構築処理*/}
    Foo( Foo const & foo){/*時間のかかるコピー処理*/}

    // あらかじめ生成しておいたFoo_dataからは、高速にコピーできる
    // 糞コード。
    Foo & operator = ( Foo_data const & data){/*それほど時間がかからず構築処理ができる*/}

// move可能でないメンバ、Foo_dataからは高速に初期化できる。
// move可能なメンバ
} ;

// これでは時間のかかるコンストラクタを二度呼び出すことになってしまう。
c.push_back(Foo(1, 2, 3)) ;

// これはそこそこ早い。
Foo_data data(1, 2, 3) ;// あらかじめ構築しておく
c.push_back(Foo()) ;// 初期化されていない空のオブジェクトを挿入
c.back() = Foo_data ;// やっとここでまともに初期化

オーケー、このコードは言うまでもなく糞だ。説明が必要なほど糞だ。問題点とは、Fooクラスの構築はとても重い処理であるということだ。だから、あらかじめ構築に必要な重い処理だけ、Foo_dataでさせておく。コンテナには、初期化されていない空のオブジェクトを挿入したあと、Foo_dataを代入して、初期化を終える。

このコードの意図は説明されなければ分からない。コードは糞だし、これを使うユーザは大変だ。しかし、この手の問題は、コンパイラの最適化ではどうにもならないところがある。では一体どうすれば良いのか。そもそも、コンテナは、内部でPlacement newを使い、オブジェクトを構築しているわけだ。そのplacement newの時にコンストラクタを呼び出すわけだが、そのコンストラクタに任意の引数を渡せれば、この問題は解決する。そのために、Variadic templatesを使えばよい。

class vector
{
public :
template < typename... Args >
void push_back( Args... args ) ;
} ;

このpush_backは、任意の数の引数を受け取る。その引数を、そのまま要素のコンストラクタに渡す。つまり、push_back(1, 2, 3)と、push_back(Foo(1, 2, 3))は同じ動作をするわけだ。ただしコンストラクタは一回しか呼び出されないので、高速である。

しかし問題が無いわけではない。そもそも初心者や、C++03プログラマが、push_back(1, 2, 3) というコードを見て何を思うか。コンストラクタ引数を渡しているとは思わないのではないか。むしろ、

c.push_back(1) ;
v.push_back(2) ;
v.push_back(3) ;

と、三つの要素を挿入できる便利な機能が付け加えれたのだと、考え違いをするのではないか。紛らわしいことに、C++0xでは、initializer listを使って、一度に三つの要素を挿入することもできるのだ。

v.push_back({1, 2, 3}) ;三つの要素を挿入する

また、この実装では、explicitなコンストラクタでも、呼び出せてしまう。なぜなら、実際にコンストラクタを明示的に呼んでいるからだ。

問題はまだある。C++ではゼロというのは特別なリテラルだ。0はどんなポインタにも代入できてしまう。しかし、C++03で合法なコードが、この提案が通った暁には、C++0xでは以下の通りになってしまう。

vector<int*> v;
v.push_back(); // オーケー、引数の無いデフォルトコンストラクタを呼び出す。
v.push_back(nullptr); // オーケー、NULLを表す新しい予約語、nullptrが導入されることが決まっている
v.push_back(0); // エラー、int *はintで初期化できない

なぜこのようなことが起こるのか。それは、最後のpush_back呼び出しは、int *に対して、int型でコンストラクタを呼び出そうとするからだ。ポインタはゼロの代入に関して特別なルールがあるが、int型全般に対して、そのような特別なルールは無い。この問題に関しては、conceptを使えば解決できる。ペーパーでは、mapコンテナのために、pairも同時にVariadic Templatesを受け取るので、言及されている。これもオーバーロードを使えば解決可能である。

もうひとつ、以前の提案では、emplaceには二つのオーバーロードがある。オブジェクトそのものをとるemplaceと、オブジェクトのコンストラクタの引数をとるためのヒントを加えたバージョンだ。しかしこれがうまく機能しないコードが発見された。ここでは詳しく説明しないのでN2680を読んで欲しいが、まあnastyな問題だ。

では解決方法は何か。まず、オーバーロードを使用しないことだ。別名をつけてやればいい。

template<typename... Args> void emplace_front(Args&&... args);
template<typename... Args> void emplace_back(Args&&... args);
template<typename... Args> iterator emplace(const_iterator position, Args&&... args);

つまり、既存のpush_backなどのメンバには手を加えないのだ。push_backは以前と変わらず、実際の値しか受け付けない。コンストラクタ引数を渡したい場合は、明示的にemplaceを使う必要がある。setの場合は、emplace_hintなどとするしかないが、まあ仕方が無いだろう。

私個人としては、この案を支持する。それに、コードが分かりやすくなる。push_backなどは既に意味が周知となっているので、この意味に別の意味を付け加えるのはよくないことだ。

もうひとつ、ファンシーなアプローチがある。ファンシーすぎて解説する意欲が沸かない。ひとつ言えることは、どう考えても糞だということだ。だから、気になるからといって、わざわざ自分で読む必要はない。もっとも、あなたがBoostの実装を気にするほどの言語マニアであれば読んでもよい。一体どこがシンプルでイージーなのかと小一時間。私の考えでは、このファンシーなアプローチを実装することは可能であると思う。Variadic templatesとTupleとメタプログラミングと、いかなる変態的コードであろうとも恐れぬ蛮勇があれば、実装可能だろう。彼のコードにはコメントが書かれているであろう。You are not expected to understand this. と。

No comments: