Boost.Move 解説 (前編)
はじめに
Boost.Move は、C++0x で導入される move sematics を C++03 でエミュレートするべく、Ion Gaztanaga 氏 (Boost.Interprocess の作者) によって開発されたライブラリです。既に Boost 入りが決定しており、現在 trunk にあります。
この記事は、その使い方と実装を日本語で解説するために書かれました。C++0x の move semantics を大まかに理解している人を対象にしています。
参照
本の虫: rvalue reference 完全解説
【5日目】Boost.Moveが気になって - Flast?なにそれ、おいしいの?
ムーブ可能なクラスを定義する
クラスを move semantics に対応させるためには、Boost.Move の作法に則って定義しなければなりません。コピー可能かつムーブ可能なクラスを定義する例を示しましょう。
#include <boost/move/move.hpp> // int * を内包するクラス class integer { BOOST_COPYABLE_AND_MOVABLE(integer) // 1. int *value_; public: integer() : value_(new int()) {} integer(int value) : value_(new int(value)) {} integer(integer const &x) // 2. : value_(new int(*x.value_)) {} integer(BOOST_RV_REF(integer) x) // 3. : value_(x.value_) { x.value_ = 0; } ~integer() { delete value_; } integer &operator=(BOOST_COPY_ASSIGN_REF(integer) x) // 4. { if (this != &x) { delete value_; value_ = new int(*x.value_); } return *this; } integer &operator=(BOOST_RV_REF(integer) x) // 5. { if (this != &x) { delete value_; value_ = x.value_; x.value_ = 0; } return *this; } };
- BOOST_COPYABLE_AND_MOVABLE(type) を private セクションに記述します。
- コピーコンストラクタは通常通り定義します。
暗黙に定義されるものを使うこともできます。*1 - ムーブコンストラクタは、引数を BOOST_RV_REF(type) として宣言します。この型は、ほとんど type & と同様に扱うことができます。
- コピー代入演算子は、引数を type const & ではなく、BOOST_COPY_ASSIGN_REF(type) として宣言します。この型は、ほとんど type const & と同様に扱うことができます。コピー代入演算子は必ず定義しなければなりません。
- ムーブ代入演算子は、引数を BOOST_RV_REF(type) として宣言します。
コピー不可かつムーブ可能なクラスを定義する場合は、マクロ BOOST_COPYABLE_AND_MOVABLE の代わりに BOOST_MOVABLE_BUT_NOT_COPYABLE を使用し、コピーコンストラクタとコピー代入演算子は宣言しません。
ムーブ可能なクラスを定義する方法はこれだけです。こうして定義したクラスは、ほとんど C++0x の move semantics に従って動作します*2。明示的に move する場合は、std::move の代わりに boost::move を使います。
では実装について考察しましょう。
Move semantics のエミュレーション
Lvalue と rvalue を識別する
コピーコンストラクタとムーブコンストラクタ、コピー代入演算子とムーブ代入演算子を呼び分けるには、lvalue と rvalue を識別することが必要不可欠です。どうすればいいでしょうか。
任意の型*3 T については、T の rvalue は T const & でしか受けることができません。しかし、T の lvalue もまた T const & で受けることができますから、一度 T const & で受けてしまうと、引数が lvalue だったのか rvalue だったのかはわからなくなってしまいます。
では、T & を受けるオーバーロードと T const & を受けるオーバーロードを用意するのはどうでしょうか。T の lvalue を渡すと前者が呼び出され、T の rvalue を渡すと後者が呼び出されるので、これで T の lvalue と T の rvalue を区別することができます。しかし T const の lvalue は後者を呼び出してしまうので、依然として T の rvalue との区別ができません。
任意の型 T についてはこれが限界です。しかし、クラス型にある型変換演算子を侵入的に定義することができれば、lvalue と rvalue を識別することが可能になります。そこで登場するのが rv
rv によるオーバーロード
rv
template <class T> class rv : public T { /* ... */ };
T から派生しただけの単純な型です。ではクラス型 hoge に、rv
class hoge { public: operator rv<hoge> &() { return *static_cast<rv<hoge> *>(this); } operator rv<hoge> const &() const { return *static_cast<rv<hoge> const *>(this); } };
static_cast*4 を使って、自らを rv
これによって何が可能になるのでしょうか。次の3つの関数を見てください。
void f(hoge &); (A) void f(rv<hoge> const &); (B) void f(rv<hoge> &); (C)
まず hoge の lvalue は、通常通り (A) を呼び出します。次に hoge const の lvalue または rvalue は、const 版の型変換演算子によって rv
結果、見事 hoge の rvalue だけを (C) に振り分けることに成功しました。hoge const の lvalue と rvalue は両方 (B) を呼び出すので区別できませんが、実質これらを区別する意義はないので、問題ないでしょう。
では最初の integer の定義に戻って、Boost.Move が何をしているのかを明らかにしましょう。
代入演算子の実装
まず最初のマクロ呼び出しに注目しましょう。
BOOST_COPYABLE_AND_MOVABLE(integer)
このマクロは二つの役割をしています。一つは前述の型変換演算子を実装すること、もう一つは代入演算子の上記 (A) 版 (integer & を引数に取るもの) を実装することです。展開してみましょう。
public: integer& operator=(integer &t) // 代入演算子 (A) { this->operator=(static_cast<const ::boost::rv<integer> &>(const_cast<const integer &>(t))); return *this;} public: operator ::boost::rv<integer>&() { return *static_cast< ::boost::rv<integer>* >(this); } operator const ::boost::rv<integer>&() const { return *static_cast<const ::boost::rv<integer>* >(this); } private:
代入演算子については (A) と (B) を区別する意義は通常ありませんから、(A) はそのまま (B) を呼び出すように実装されています。ではその (B) はどこにあるのでしょうか。それは、我々が自らコピー代入演算子として実装しました。
integer &operator=(BOOST_COPY_ASSIGN_REF(integer) x); // コピー代入演算子 (代入演算子 (B))
コピー代入演算子*5の引数を integer const & ではなく BOOST_COPY_ASSIGN_REF(integer) として宣言した理由が、ここで明らかになります。BOOST_COPY_ASSIGN_REF(integer) は boost::rv
最後にムーブ代入演算子を見てみましょう。
integer &operator=(BOOST_RV_REF(integer) x); // ムーブ代入演算子 (代入演算子 (C))
もう説明の必要はないでしょう。BOOST_RV_REF(integer) は boost::rv
コンストラクタの実装
コピーコンストラクタとムーブコンストラクタの呼び分けは、少々事情が違います。我々は integer のコピーコンストラクタを、integer const & を引数に取るように定義しました。
integer(integer const &x);
T const & は、lvalue と rvalue の区別をつかなくしてしまう凶器でした。実際その通りなのです。次のコードを考えます。
test.cpp
#include <iostream> #include <boost/move/move.hpp> class test { BOOST_COPYABLE_AND_MOVABLE(test) public: test() {} test(test const &) { std::cout << "copy constructor\n"; } test(BOOST_RV_REF(test)) { std::cout << "move constructor\n"; } test &operator=(BOOST_COPY_ASSIGN_REF(test)) { return *this; } }; test create_test() { return test(); } int main() { test t(create_test()); }
コンパイルして実行します。
$ g++ -fno-elide-constructors test.cpp $ ./a.exe copy constructor copy constructor
ムーブコンストラクタが呼び出されるべきところ、コピーコンストラクタが呼び出されてしまっています。
しかし心配は無用です。今回は -fno-elide-constructors オプションで意図的に抑制しましたが、この場合は RVO という最適化によって、コピーコンストラクタの呼び出しを省略することができるのです。ムーブより RVO の方がさらに高速なため、Boost.Move はそれを殺さない戦略をとっているということです。
ムーブコンストラクタを呼び出さなくてはならない場合は、明示的に boost::move を呼び出すことによって、T を boost::rv
基底クラスまたはメンバオブジェクトを持つクラスのムーブ
実装の解説は一旦終わりにして、基底クラスまたはメンバオブジェクトを持つクラスをムーブ対応させる方法を示しましょう。
class derived : public base { BOOST_COPYABLE_AND_MOVABLE(derived) member mem; public: derived() : base(), mem() {} // コピーコンストラクタ derived(derived const &x) : base(x), mem(x.mem) {} // ムーブコンストラクタ derived(BOOST_RV_REF(derived) x) : base(boost::move(static_cast<base &>(x))), // 1. mem(boost::move(x.mem)) {} // コピー代入演算子 derived &operator=(BOOST_COPY_ASSIGN_REF(derived) x) { base::operator=(x); mem = x.mem; return *this; } // ムーブ代入演算子 derived &operator=(BOOST_RV_REF(derived) x) { base::operator=(boost::move(static_cast<base &>(x))); // 2. mem = boost::move(x.mem); return *this; } };
基本的には C++0x での定義と変わりありません。しかし重要な違いがあります。コメント 1. 2. の箇所に注目してください。BOOST_RV_REF(derived) x として受け取った引数を、base & にキャストしてから boost::move に渡しています。
なぜ boost::move(x) では駄目なのでしょうか。それは、boost::move(x) によって得られる rv
派生クラスでムーブコンストラクタ・ムーブ代入演算子を定義する場合は、この点に注意しなければなりません。