2014-04-21-01-本気で move について考える
>> Site top >> weblog >> 月別アーカイブ >> 2014年04月のlog >> 2014-04-21-01-本気で move について考える
最終更新日付:2014/04/21 01:00:00
本気で move について考える
2014 年 04 月 21 日
さて、move について本気で考えなければならないところまで来てしまったようだ。C++ 11 の新機能の中でも、自分にとっては一番得体の知れない move。そんな気がするだけかもしれない move。さて、どうなることやら。
さて、そもそも move とは「右辺値参照」という用語とともに使われるアレだった。右辺値参照というのは、例えば一時オブジェクトのように、const 参照でしか取れないはずのものを変更可能な参照として取ることができるようにするもののようだ。なんだか聞いているだけで危なっかしい匂いがしてくるが、そのための『安全だよ』意思表明として使われるのが move らしい。というか、どちらかといえばコンパイラがプログラマに対して言質を取っているような気がする。それ以前にこんな理解でいいのか自分。
実際に試してみようと四苦八苦してみると、おおむね以下のようにすればよいことがわかった。下記コードでも std::move がないとムーブは実現しない。これが最初につまづいた点だ。
void test( std::vector<int>&& v ) { std::vector<int> tmp = std::move( v ); : : }
とりあえずの動作確認として書いたのが以下。test の呼び出しで使っている std::move は、ないとコンパイルエラーになる。test 内部使っている方の move は、なくてもコンパイルには通るがムーブが行なわれない。つまり、明示的に右辺値参照として渡したければ std::move が必要で、また渡された右辺値参照を実際にムーブ元として使いたければやっぱり std::move が必要ということになるようだ。
void test( std::vector<int>&& v, bool bMove ) { if( bMove ) { std::vector<int> x = std::move( v ); std::cout << "In test, v.size = " << v.size() << std::endl; std::cout << "In test, x.size = " << x.size() << std::endl; } } int main( void ) { std::vector<int> v1 = { 0, 1, 2, 3, 4, 5, 6 }; std::vector<int> v2 = std::move( v1 ); std::cout << "In main, v1.size = " << v1.size() << std::endl; std::cout << "In main, v2.size = " << v2.size() << std::endl; test( std::move( v2 ), true ); std::cout << "In main, v2.size = " << v2.size() << std::endl; return 0; }
ちなみに、test 呼び出しで第2パラメータを false にすると main の v2 の内容は空にならない。このことから、std::move を通して右辺値参照を他の関数に渡したとしても、実際にムーブ元として使われない限りは中身は無事なようだ。
で、これを CL-STL でどう実現するか。これは非常に悩ましい。単純に setf や opr= の代入元として、コピー後に代入元をクリアするというだけなら簡単にできる。実際、最初に考えたのは以下のようなものだった。
(defmacro move (expr) (multiple-value-bind (vars forms var set ref) (get-setf-expansion expr) (let ((ret (gensym "RET"))) `(let* (,@(mapcar #'list vars forms)) (let ((,@var nil) (,ret ,ref)) ,set ,ret)))))
これなら、以下のような初歩的なムーブは簡単だ。
* (let ((v1 nil) (v2 10)) (setf v1 (move v2)) (values v1 v2)) => 10 NIL
しかし。これだけではまだ全然ダメなわけで、move はさらに以下の要件を満たさなければならない。
- move の結果を他の処理に『渡す』ことができ、その先でムーブが実行できなければならない。
- 上記の方法で渡しても、普通の参照のようにも使えなければならない。
- 実際にムーブを実行する場合、もう一度 move を使う。
これは、少なくとも自分にとっては相当な難問だ。というか、「普通に参照渡しされた変数としても使えるが、move にかければムーブ元になる」って、どうやって実現すればいいんだろう?
とりあえず、「普通の変数としても使える」という部分は後回しだ。move にかければ「ムーブ元になれる何か」が得られ、その「ムーブ元になれる何か」を move にかければムーブを実行できる。これをなんとかするには、クラスと総称関数を使うしかないだろう。つまり、最初の move 呼び出しにおけるレキシカルコンテキスト内でクロージャを作成してそれをクラスインスタンスに閉じ込めるのだ。当面、この「ムーブ元になれる何か」を表すクラスを remove-reference と呼ぶことにしよう。
さて、上記のようなクロージャを作成できるのはマクロだけだ。しかし、次の move 呼び出しでは move 自身が総称関数的に動作しなければならない。つまり、move はマクロだが、「実行時に」remove-reference が渡された時だけは別の動きをしなければならないのだ。
‥‥‥で、色々と頭をひねり、いくつかのポイントをあきらめた結果としてできあがったのが以下のコードだ。あきらめたポイントというのは、「普通の変数としても使える」という部分だ。
(defclass remove-reference () ((closure :type :function :initarg :closure :accessor __rm-ref-closure))) (defun __move-1 (closure) (let ((val (funcall closure :get))) (if (eq (type-of val) 'remove-reference) val (make-instance 'remove-reference :closure closure)))) ;; never expanded by move macro. (defgeneric __move-2 (lhs rhs)) (defmethod __move-2 (lhs rhs) (declare (ignore lhs)) (values rhs rhs)) (defgeneric __move-3 (first last result)) (defmacro move (&rest args) (let ((cnt (length args))) (if (= cnt 1) (multiple-value-bind (vars forms var set ref) (get-setf-expansion (car args)) `(__move-1 (let* (,@(mapcar #'list vars forms)) (let ((,@var nil)) (lambda (op) (case op (:get ,ref) (t (progn (setf ,@var op) ,set nil)))))))) `(__move-3 ,@args))))
これにさらに、opr= マクロから展開される総称関数 operator= の特定化として、右辺が remove-reference インスタンスだった場合の実装が以下のように追加される。
(defmethod operator= (a (b remove-reference)) (multiple-value-bind (lhs rhs) (__move-2 a (funcall (__rm-ref-closure b) :get)) (funcall (__rm-ref-closure b) rhs) lhs))
‥‥‥まだマズいところがたくさんあるとは思うが、疲れてしまったので続きは明日以降。
コメント
このページのタグ
Page tag : Common Lisp
Page tag : STLとその移植
Copyright(C) 2005-2021 project-enigma.
Generated by CL-PREFAB.