Boost.Move 解説 (後編)
前回の続きです。今回は、Boost.Move による引数の転送を中心に解説していきたいと思います。
前回の補足
Boost.Move を使う利点
重要なことに触れていませんでした。
Move semantics は、乱暴に言えば「死すべきオブジェクト (rvalue) からは破壊的コピーを行うことで、パフォーマンスを改善する」ことを可能にするためのものでした。Boost.Move の第一の目的は、これを C++03 でエミュレートすることにあります。
しかし、Boost.Move を使う意義はそれだけではありません。Boost.Move は、rvalue references が使用できるコンパイラにおいては、それらを利用したコードを生成するのです。たとえば BOOST_RV_REF(T) は、C++03 モードでは boost::rv
したがって Boost.Move は、「現在は C++03 でコードを書いているが、将来は C++0x に移行する (かもしれない)」という場合に、最大の価値を発揮すると言えるでしょう。
boost::rv について
本記事では、説明のために boost::rv の名前を登場させていますが、これはユーザーに公開されていません。ユーザーコードでは、BOOST_RV_REF(T) などのマクロを展開した結果として、あるいは boost::move の戻り値の型としてのみ存在します。
なぜ boost::rv は公開されていないのでしょうか。その理由は、上で述べた、C++0x でも最適なコードを生成することと関係します。すなわち、BOOST_RV_REF(T) を T && に展開し、boost::move が T && を返すようにすれば、すぐに rvalue references に対応したコードに切り替えることができます。しかし、ユーザーが boost::rv を直接使っていると、この切り替えを阻害してしまいます。そのために、boost::rv はユーザーに公開されていないのです。
コンストラクタに引数を転送する
説明の前にコードを示しましょう。引数 1 つをコンストラクタに転送するファクトリ関数です。
template <class T, class A1> T *create(BOOST_FWD_REF(A1) a1) { return new T(boost::forward<A1>(a1)); }
C++0x での perfect forwarding と非常によく似ています。書き方で迷うことはないでしょう。
しかしこの転送は完璧ではなく、制限もあります。ここでは実装を解説しながら、制限を明らかにしていく形を取ろうと思います。
転送の実装と制限
まず、オーバーロードが 1 つしかないことから、引数の型 BOOST_FWD_REF(A1) は必然的に A1 const & となります。任意の型の lvalue と rvalue を等しく受け入れるためには、const lvalue reference を使うしかないからです。
すると、ムーブ可能な型 Movable についても、その rvalue は const lvalue として転送されることになります。これでは move semantics を活用できないように思えます。
そこで、boost::move によって生成された rv
賢明な読者は、create に rv
では、この転送の制限をまとめましょう。
「rv
この制限を受け入れられるか
この制限の最大の問題は、non-const lvalue を const lvalue として転送してしまうことでしょう。関数が X & を期待しているところに、X const & を渡すようなことになっては大変です。
しかしコンストラクタに転送する場合に限って言えば、多くのコンストラクタは引数を値か const 参照でしか受け取りませんから、問題は生じにくくなります。そこに妥協の余地があると言えるでしょう。
またたとえ制限を付けてでも、オーバーロードを 1 つに押さえたことによって、重要な 2 つの利点が生まれています。
1 つは、オーバーロード爆発の回避です。たとえば、引数 1 つに付き 2 つのオーバーロードが必要だとするとどうでしょう。引数が 2 つならオーバーロードは 4 個、3 つなら 8 個と、引数が増えるとオーバーロードの個数は O(2^n) で増えていきます。これは実装上の問題となります。
またもう 1 つの利点として、先に述べた、C++0x で最適な動作をするコードに変換できるということがあります。上のコードが C++0x の perfect forwarding に似ていたのは偶然ではありません。BOOST_FWD_REF(A1) を A1 && に、boost::forward を std::forward (相当のもの) に置き換えることによって、それはまさしく C++0x の perfect forwarding となるのです。しかし、オーバーロードが複数あっては、これを行うことが難しくなります。
Boost.Move による転送の制限と、それに妥協しうるいくつかの理由を示しました。これらを総合して、この転送を用いるかどうかを決めることになるでしょう。
では次に、この転送を用いないことを選んだ場合に、どのような代替策があるのかを検討しましょう。
Perfect forwarding
上記の方法では、non-const lvalue を正しく転送できないことが最大の問題でした。オーバーロード爆発を許容することにすれば、この問題を解決できます。実装上の問題については、プリプロセッサを使ってオーバーロードを生成することで解決できるでしょう。C++0x 環境で最適なコードに展開されない点については、C++0x 用の実装 (perfect forwarding) を別に用意することになります。
この方法の実装は単純です。引数ごとに T & で受けるオーバーロードと T const & で受けるオーバーロードを用意し、受けたらそのまま次の関数に渡すのです。コードを示しましょう。
template <class T, class A1> T *create(A1 &a1) { return new T(a1); } template <class T, class A1> T *create(A1 const &a1) { return new T(a1); }
先ほど転送に難儀した rv
一方、rvalue はやはり const lvalue として転送されます。ムーブに対応している型にとっては、不満が残る結果です。それらの型の rvalue は、rv
追記: 呼び出す側で、rvalue を明示的に rv
hoge *p = create<hoge>(static_cast<BOOST_RV_REF(movable)>(movable()));
その他の機能
簡単に紹介します。
Move iterators
namespace boost { template <class Iterator> class move_iterator; template <class Iterator> move_iterator<Iterator> make_move_iterator(Iterator const &i); }
Move algorithms
namespace boost { template <class InputIterator, class OutputIterator> OutputIterator move(InputIterator first, InputIterator last, OutputIterator result); template <class BidirectionalIterator1, class BidirectionalIterator2> BidirectionalIterator2 move_backward(BidirectionalIterator1 first, BidirectionalIterator1 last, BidirectionalIterator2 result); template <class InputIterator, class ForwardIterator> ForwardIterator uninitialized_move(InputIterator first, InputIterator last, ForwardIterator result); template <class InputIterator, class ForwardIterator> ForwardIterator copy_or_move(InputIterator first, InputIterator last, ForwardIterator result); template <class InputIterator, class ForwardIterator> ForwardIterator uninitialized_copy_or_move(InputIterator first, InputIterator last, ForwardIterator result); }
move、move_backward、uninitialized_move は、C++0x の同名のアルゴリズムと同じです。
copy_or_move、uninitialized_copy_or_move は、それぞれ copy と uninitialized_copy を move_iterator に対応させたものです。
Boost.Move を使う上での注意点
1 つだけ、Boost.Move を使う上での重要な注意点を挙げておきましょう。
C++0x では、以下のコードはコンパイルできません。
void f(int &&); // ... int i; f(i);
Lvalue は rvalue reference に束縛できないのです。
しかし Boost.Move では、次のコードがコンパイルできてしまいます。
void f(BOOST_RV_REF(movable)); // ... movable m; f(m);
前回解説したように、(ムーブ可能な型) movable には rv
この問題に対しても、警告は発生しません。ユーザーが気をつけてコーディングする必要があります。上の例では、f にオーバーロードを追加することで、問題を回避できます。
void f(movable &); // (A) void f(BOOST_CATCH_CONST_RLVALUE(movable)); // (B) void f(BOOST_RV_REF(movable)); // (C)
前回解説した、代入演算子の実装と同じです*1。(C) はもはや、movable の non-const rvalue しか受け付けません。
最後に
Boost.Move のインターフェースと実装を、その rationale を交えながら解説しました。解説は trunk の revision 70924 を元に行っています。その後変更が生じるかもしれないことをご了承ください。