にゃははー

はへらー

【5日目】Boost.Moveが気になって

C++ Advent Calendar 2010も5日目を迎えました。

どーでもいいけど、本日はBoostjp忘年会です!!そしてこのページの読書会はやりません。(キリッ

さてさて、多くの人が待ち望んでいるC++0xですが、C++をいじり倒してる人もそうでない人も、触れる機会がダントツで多いかもしれないR-value referenceをC++03でエミュレートしたBoost.Moveについて今回は触れようかと思います。
以後*-value referenceを*v-refに略します。

(というか私がこういう記事(解説とかその類い)を書くのってあんまり無いな・・・
※突っ込むところがあったら遠慮なく・・・

まずはおさらいから

多分ここに飛んできたということはAdvent Calendarから来たと思うので、Rv-refについてはどっかの人がやってくれてていて問題ないと思いますが、まぁ簡単に使い方から。 (あれ・・・だれも・・・やってな・・・ 見なかったことにしようワハハハハハハ
といっても適当なコード片です。

// int &ref = 0;	// bad
const int &cref = 0;
// cref = 1;	// bad
int &&rref = 0;
rref = 1;

こんなん見せられても「だからどうした」って感じですかね。じゃぁこれはどうしましょう。

struct S : boost::noncopyable
{
	...
	void f( void ) { ... }
};

S hoge( void ) { ... }

int main( void )
{
	S s = hoge(); // Oops!
	s.f();
}

hoge()はどうやってSのインスタンスを返したとかは省略します。

どうしてもS::fを呼びたいからconst Lv-refじゃだめ。だけどSのインスタンスは下手なコピーしてほしくない。
たまにあります。*1

これを解決するのがC++0xのRv-refです。

OK、わかった。で?

と、前置きが長くなったけどこれをC++03でやりたいねって試みがBoost.Move。
まずはDLしましょう。 zipはここ にあります。
sandboxはここ ドキュメントはここ にあるので+αでやりたい人は各自参照してください。

ところでこいつは最高にcoolで、const_castとreinterpret_castってのを使って解決していて、つまりやってることはこう。

struct S
{
	...
	S( S &s ) { ... }
	...
	void f( void ) { ... }
	...
};

S hoge( void ) { ... }

int main( void )
{
	S s = const_cast< S & >( static_cast< const S & >( hoge() ) );
	s.f();
}

これならS::S( S &s )でsを書き換えつつ所有権の移動(Move)ができます。

※ただし、現在の実装ではboost::moveを使ってstd::moveの様に一時オブジェクトのmoveはできません。明示的にboost::forwardを使用する必要があります。これはC++03ではconst Lv-refが一時オブジェクトによるものなのか、const Lv-refなオブジェクトなのかが区別できないことに起因すると思われます。

所有権の移動って?

これこそRv-refの本領です(だと私は勝手に思ってます)。
詳しい解説は省略しますが、C++03のstd::auto_ptrテンプレートクラスとC++0xのstd::unique_ptrテンプレートクラスの差を考えてもらえればわかり易いと思います。

つまり、newなどでヒープ上に構築したオブジェクトなどを以下のようにコピーすることはよくあると思います。(shared_ptr使えとかそういうつっこみは無しで...

class S
{
	int *array;
public:
	S( void ) : array( new int [ 10 ] ) {}
	S( const S &s ) : array( new int [ 10 ] )
	{ memcpy( this->array, s.array, 10 ); }
	~S( void )
	{ delete [] this->array; }
};

今回はS::arrayの要素数は高々10でそれ程コストは高くありませんが、これが数千とか数万とかになると話は変わってきます。しかもコピー元オブジェクトはもう使わないとしたら悲しくなります。*2

copy ctorがLv-refを受け取るんだとしても、一時オブジェクトは受け取れないので、関数の戻り値としては使用できません。

Rv-refは一時オブジェクトを束縛することが出来るので上記の問題は解決できます。つまり、

class S : public boost::noncopyable // 0x的にはdelete指定しろって感じですgggg...
{
	int *array;
public:
	S( void ) : array( new int [ 10 ] ) {}
	S( S &&s ) : array( s.array ) // move constructor
	{ s.array = NULL; } // 0x的にはnullptrをt...(ry
	~S( void )
	{ delete [] this->array; }
};

とすることで、''コピー''ではなく、''移動''することができます。もちろん移動元のオブジェクトはそれ以後使用するのはダメです。移動してそのオブジェクトは無いことになっています。*3

(詳しくないけど十分?

大体解った(気になった)からBoost.Moveの使い方を教えてくれ

まず、先のアドレスからDLしたら、その中の boost/move/move.hpp をインクルードします。これだけでとりあえずのことはできるようになります。

Lv-refは型名に&を付けるだけでしたが、Boost.MoveのRv-refはちょっと違います。
まずクラスTに対するRv-refはBOOST_RV_REF( T )となります。
そしてstd::moveにあたるのはboost::moveで、std::forwardにあたるのがboost::forwardとなります。

また、Rv-refにしたいクラスTは以下のどちらかのように定義されている必要があります。

// moveは出来るがcopyはできない
class T
{
	BOOST_MOVABLE_BUT_NOT_COPYABLE( T ) // ; を入れるとwarningが出る
};

// copyもmoveも可能
class T
{
	BOOST_COPYABLE_AND_MOVABLE( T ) // ; を入れるとwarningが出る
public:
	// const Lv-ref を受ける copy assignment
	T &operator=( BOOST_COPY_ASSIGN_REF( T ) ) { ... }
};

BOOST_MOVABLE_BUT_NOT_COPYABLEの注意点として、マクロ展開の際にcopy ctor/assignmentがprivateで宣言されてしまいます。

BOOST_COPYABLE_AND_MOVABLEの注意点は、マクロ展開で暗黙的に定義されるcopy assignmentがconst Lv-refを受けるcopy assignmentを要求する*4ことです。copy assignmentにはBOOST_COPY_ASSIGN_REFマクロを使うと良いでしょう。
BOOST_MOVABLEみたいな変換関数だけが定義されたマクロがあればよかったんだけどな・・・

また、両マクロは最終的にクラスのアクセス指定子をprivateにするので、structを使っている時やpublic指定の範囲内での使用は注意してください。(この挙動は大抵のBoostのライブラリと同じなのかな?

で、BOOST_MOVA.../BOOST_COPYA...が定義されていないクラスはboost::moveやboost::forwardを使ってもただの(const)Lv-refが返ってくるだけです。というのもこれらのマクロはBOOST_RV_REF用の変換関数を定義し、その中でreinterpret_castを使ってるわけです。
そしてboost::forwardに至っては最高にふざけたcoolなconst_castを使って一時オブジェクトのperfect forwardingをエミュレートしています。

ちなみにBOOST_RV_REF( T )という型経由でメンバを参照するのは普通のLv-refと同様にできます。これはTから派生した型への参照を使っているからです。詳細な実装はヘッダを読んでくだしあ。

簡単な例

以下に簡単な例を示します。関数の戻り値として使用することももちろんできるので試してみてください。

#include <iostream>
using namespace std;

#include <boost/move/move.hpp>

struct S
{
	BOOST_COPYABLE_AND_MOVABLE( S )

public:
	S( void )
	{ cout << "\tdefault ctor" << endl; }

	S( BOOST_COPY_ASSIGN_REF( S ) )
	{ cout << "\tcopy ctor" << endl; }

	S( BOOST_RV_REF( S ) )
	{ cout << "\tBoost.Move ctor" << endl; }

	void f( void )
	{ cout << "\tcall S::f()" << endl; }

	void f( void ) const
	{ cout << "\tcall S::f() const" << endl; }

	S &operator=( BOOST_COPY_ASSIGN_REF( S ) s )
	{
		cout << "\tcopy assignment" << endl;
		return *this;
	}

	S &operator=( BOOST_RV_REF( S ) s )
	{
		cout << "\tBost.Move assignment" << endl;
		return *this;
	}
};

int main( void )
{
	cout << "constructions" << endl;
	S s;					// default
	S c = s;				// copy
	S m = boost::move( s );	// move

	cout << "temporary object" << endl;
	S fm = boost::forward< BOOST_RV_REF( S ) >( S() ); // default and move
	// S fm = std::move( S() ); // みたいにはできない

	cout << "assignments" << endl;
	c = s;				// copy
	m = boost::move( s );		// move

	cout << "const Lv-ref: ";
	const S &lr = s;
	lr.f();					// const member call

	cout << "Rv-ref: ";
	BOOST_RV_REF( S ) rr = s;
	rr.f();					// member call
}

出力は

$ ./a.out
constructions
	default ctor
	Boost.Move ctor
temporary object
	default ctor
	Boost.Move ctor
assignments
	copy assignment
	Bost.Move assignment
const Lv-ref: 	call S::f() const
Rv-ref: 		call S::f()

となりました。copy ctorが無いですが、最適化の結果無かったことにされたようです。しかし無かったことになったということは、確実にcopy ctorが呼ばれるだろうことはわかります。

OK把握した。で次は?

ドキュメント頑張って読んでくだしあ。私もまだ手探りでやってる状態なのでやってませんが、move_iteratorとかmove_adopterとかあるので頑張ってねー (すごい投げたけどキニシナイ

さて、C++ Advent Calendar jp 2010、明日はid:tt_clownさんです。 C++は多分食べられないです。むしろ食べられます。アーッ!

*1:ただし、前日のid:gintenlabさんの記事を読んだ方はOPCでえーやんとなりますね。

*2:だからOPCしろt(ry

*3:コンパイルも実行もできますが良くないです

*4:正確にはconst BOOST_RV_REF()を受けるcopy assignmentですが、ダウンキャストされるのでconst Lv-refと考えといて問題ないでしょう。