読者です 読者をやめる 読者になる 読者になる

'(you lisp me)

Lispって何だ

構造体とクラスにおける循環参照

よくある階層構造(親子関係)を構造体とクラスで表現してみる。

;;;; 構造体の定義
(defstruct h-struct
  (val 0)
  (parent nil)
  (child nil))
;;;; クラスの定義
(defclass h-class ()
  ((val :accessor get-val
        :initarg :val
        :initform 0)
   (parent :accessor get-parent
           :initarg :parent
           :initform nil)
   (child :accessor get-child
          :initarg :child
          :initform nil)))

これを使って2つのオブジェクトを相互に参照させてみます。
まずは構造体の例。

;;;; 構造体における循環参照
(setf mother (make-h-struct :val 50))
(setf me (make-h-struct :val 20))

(setf (h-struct-child mother) me)
(setf (h-struct-parent me) mother) ;; ここで循環参照が完成

しかしスタックオーバーフローでエラー。止まります。
今度はクラスでやってみます。

;;;; クラスにおける循環参照
(setf you (make-instance 'h-class :val 25))
(setf son (make-instance 'h-class))

(setf (get-child you) son)
(setf (get-parent son) you) ;; ここで循環参照が完成

これは大丈夫。
何が違うのだろうか。

なんとなく構造体の例で参照先のフィールドを書き換えてみる。

(setf (h-struct-val (h-struct-child mother)) 30)
(print (h-struct-val me)) ;; 値渡しなら20のままのはず

;;;; 結果
30
30

どうやら参照が渡されているようだ。
一方クラスの場合。

(setf (get-val (get-parent son)) 20)
(print (get-val you)) ;; 値渡しなら25のままのはず

;;;; 結果
20
20

エラーが起きるか否かという違いを除いて、どちらも同じような結果を示した。
一体内部で何が起こっているんだろうか。そして構造体でも循環参照を実現する方法はあるのだろうか(ぶん投げ)。

結果はsbclとcclで確認しました。
処理系じゃなくてもっと根本的な問題でしょう。

、いう内容で投稿しようと思ってたのですが…よく考えるとこれはREPLの問題ですね。
構造体のスロットを書き換えた時、REPLは構造体の中身を全て表示しようとするはずです。
一方クラスの場合はオブジェクト名が表示されるだけに留まります。

循環リストは*print-circle*をtにしてやれば表示が出来ました。
構造体の表示も結局はリストです。同様の効果が得られるでしょう。

(setq *print-circle* t)

こうしておけば構造体であっても循環参照が可能です。
あーすっきりした。

ちなみにC言語の場合は単純にparentとchildをポインタ型にすれば出来ます。
いわゆる自己参照構造体ってやつです。

今回のはある意味、REPLの弊害と言えるのではないでしょうか。
なんとなくデフォルトでtにしておいて欲しいと思うのですが…。