2014-04-30-01-代入と move:気付いたことをいくつか - project-enigma

2014-04-30-01-代入と move:気付いたことをいくつか

>> Site top >> weblog >> 月別アーカイブ >> 2014年04月のlog >> 2014-04-30-01-代入と move:気付いたことをいくつか

最終更新日付:2014/04/30 23:50:00


代入と move:気付いたことをいくつか

2014 年 04 月 30 日

ごちゃごちゃとコードをイジっているうちに、気がついたら代入構文を実装してしまった。反省しつつ、気付いたことを。

 

マクロ opr= が第一パラメータを代入先の場所とみなし、総称関数 operator= をコールして‥‥‥というのは問題ないようなのだが、良く考えてみると以下のような書き方ができないことがわかる。

(stl:opr= (stl:opr= foo 10) 20)

 

加算と代入、および減算と代入の合わせ技である += / -= なども同様だ。C++ では通常、代入演算子は代入先の『非 const 参照』を返す。だから、代入式が返す値に対してさらに代入をかけることができる。つまり、(foo = 10) = 20; ということだ。しかし、例の opr= マクロは結果として左辺の『値』を返すことしかできない。そりゃそうだ。Common Lisp には(C++ の文脈でいうところの)「参照」などというモノは存在しないのだから。

では、自分でこういう書き方をするだろうか? 絶対ノーだ。しかし、可能な限り「同じことができる」ようにはしておきたい‥‥‥その一方で、現時点でも「やりすぎだ」という声が(自分の中から)聞こえてきている。自分がそもそもやりたかったのは STL のパワーを Common Lisp でも使えるようにすることだった。決して、Common Lisp の上に C++ を作り込むことではない。だったら無理をすることはないじゃないか‥‥‥うん、そう考えることにしよう。

 

さて、気になっていることはまだある。move 代入のトリガーとなる operator= の特定化、あれに穴があくことはないだろうか、ということだ。どういうことか、以下に。

まず、代入構文の opr= マクロは、なんやかやで以下の総称関数 operator= 呼び出しに展開される。デフォルトでは「右辺の値」をそのまま返す。ここではデバッグ用の情報だけを出力しておく。

(defgeneric operator= (lhs rhs))

(defmethod operator= (lhs rhs)            ; [A]
  (format t "(operator= T T) called.~%")
  rhs)

 

さて、remove-reference の登場だ。(stl:opr= foo (stl:move bar)) みたいな記述でムーブの動作をさせるため、operator= の特殊化を以下のようにしておく。実際には、ここから先日 の総称関数 operator-move がコールされることになるわけだ。

(defclass remove-reference () ())

(defmethod operator= (lhs (rhs remove-reference))        ; [B]
  (format t "(operator= T remove-reference) called.~%")
  rhs)

 

自分が心配しているのは、CL-STL 自身が関知しないところで operator= のメソッドがさらに追加されることにより、代入元に remove-reference が指定されても上記のメソッドがコールされない場合があるのではないか、ということだ。総称関数呼び出しが実行時に解決される仕組みを考えると、それはどうやらありそうに思える。とりあえず、なんらかのクラス同士の『代入』を考えるとしよう。以下のように。

(defclass foo () ())
(defmethod operator= ((lhs foo) (rhs foo))        ; [C]
  (format t "(operator= foo foo) called.~%")
  rhs)

(defclass bar () ())
(defmethod operator= ((lhs bar) (rhs bar))        ; [D]
  (format t "(operator= bar bar) called.~%")
  rhs)

 

この状態でも、foo や bar の代入において代入元を remove-reference で括った場合でも意図した動作をする。つまり、ちゃんと [B] がコールされるのだ。以下のように。

* (let ((foo (make-instance 'foo))
        (bar (make-instance 'bar))
        (rm  (make-instance 'remove-reference)))
    (operator= foo foo)
    (operator= bar bar)
    (operator= foo bar)
    (operator= foo  rm)
    (operator= bar  rm)
    (values))

=> (operator= foo foo) called.
   (operator= bar bar) called.
   (operator= T T) called.
   (operator= T remove-reference) called.
   (operator= T remove-reference) called.

 

ここまではいい。しかし、(まぁ考え難いが)以下のようにして foo がどんな代入元でも受け付けるようにした途端、[B] はコールされなくなってしまう。

(defmethod operator= ((lhs foo) rhs)              ; [E]
  (format t "(operator= foo T) called.~%")
  rhs)

* (let ((foo (make-instance 'foo))
        (bar (make-instance 'bar))
        (rm  (make-instance 'remove-reference)))
    (operator= foo foo)
    (operator= bar bar)
    (operator= foo bar)
    (operator= foo  rm)
    (operator= bar  rm)
    (values))

=> (operator= foo foo) called.
   (operator= bar bar) called.
   (operator= foo T) called.
   (operator= foo T) called.
   (operator= T remove-reference) called.

 

これはまぁ、当たり前の動作だ。パラメータの左から右に向かって順番に、もっとも特定的なメソッドが選択されるのだから。最初のパラメータで foo がそのものズバリでマッチし、2番目のパラメータがいかなる条件も要求していなければたしかにそうなる。そうならないようにしたければ、第2パラメータが remove-reference の場合で個別に特定化するしかないだろう。それはそうなのだが、それをユーザコードに強いるのが嫌なのだ。

ひとまずのところ、これをエレガントに解決する方法は思いつかない。思いついたことと言えば、opr= マクロの時点で operator-moveoperator= を振り分けることくらいだった。しかし、それも何か間違っているような気がするのだ。自分がまだ良く知らない総称関数の仕組みの中に、何か役に立つモノはあるだろうか。調べてみよう。

 

コメント

このページにコメントする

 

このページのタグ

Page tag : Common Lisp

Page tag : STLとその移植

 

 


Copyright(C) 2005-2017 project-enigma.
Generated by CL-PREFAB.