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 & に展開されますが、rvalue references が有効なモードでは T && に展開されます。すなわち、一度 Boost.Move を使って書かれたコードは、C++03 と C++0x の両方で、最適な振る舞いをするようになっているのです。
したがって 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 & が渡された場合を想定します。a1 は rv const & となりますが、元は rv & であったものですから、rv & として転送しなければなりません。そこで boost::forward に、rv const & を rv & に const_cast して返すようにお願いしましょう。これで rv & → rv & の転送が可能になります。
賢明な読者は、create に rv const & を渡した場合に問題が生じることに気づくかもしれません。この場合でも、boost::forward は const_cast を行うからです。しかし心配は要りません。boost::move は、rv const & を返すことは決してないのです (Movable const を渡しても、Movable const & が返ってくるだけです)。したがって、create に rv const & を渡すような状況はまず起こらないと考えられます。
では、この転送の制限をまとめましょう。
「rv & は rv & として転送するが、それ以外は全て const lvalue として転送する。」

この制限を受け入れられるか

この制限の最大の問題は、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 & という形で転送されるべきなのです。しかし、A1 を推論によって決定する限り、根本的には解決できない問題です。
追記: 呼び出す側で、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);

}

C++0x の move_iterator と同じです。

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 & への型変換演算子が備わっています。この変換は lvalue についても有効です。その結果、lvalue を BOOST_RV_REF(T) で受けられるという奇妙な (そして危険な) 事態になってしまうのです。
この問題に対しても、警告は発生しません。ユーザーが気をつけてコーディングする必要があります。上の例では、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 を元に行っています。その後変更が生じるかもしれないことをご了承ください。

*1:BOOST_CATCH_CONST_RLVALUE(movable) は boost::rv const & に展開されます