2014-04-10-01-マクロと総称関数で関数オーバーロードを真似る - 2 - project-enigma

2014-04-10-01-マクロと総称関数で関数オーバーロードを真似る - 2

>> Site top >> weblog >> 月別アーカイブ >> 2014年04月のlog >> 2014-04-10-01-マクロと総称関数で関数オーバーロードを真似る - 2

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


マクロと総称関数で関数オーバーロードを真似る - 2

2014 年 04 月 10 日

以前、C++ の関数オーバーロードを Common Lisp のマクロと総称関数を使って真似る、という話を書いた。あれから、結局その方法を採用してしまったのだけれど、まぁやっぱり想定していなかったことは起こるわけで。それについて、今回もまたその是非に自信のない対処のことを書くとしよう。

 

とりあえず、コンテナクラスの insert メンバ関数を例にしよう。これは vector, deque, list といったシーケンスコンテナ、および set や map などの連想コンテナに備わっており、パラメータ数から異なるオーバーロードがいくつもある。詳細は省くとして、今回は set コンテナについて。

set には、insert メンバ関数に以下の3種類のオーバーロードがある。

  1. 挿入する値だけを指定するもの
  2. パフォーマンス上のヒントとして反復子も指定するもの
  3. 反復子によるシーケンスを指定して複数の値をいっぺんに挿入するもの

パラメータ数は上から順に 1, 2, 2 となるが、CL-STL ではメソッド呼び出しの対象となるコンテナオブジェクトを最初のパラメータとして取ることにしているため、パラメータ数は 2, 3, 3 となる。ちなみに、シーケンスコンテナではパラメータ数が4つになるものもある。

で、数日前に書いた方法でもってオーバーロードしたのが以下。

(defgeneric __insert-2 (container arg))
(defgeneric __insert-3 (container arg1 arg2))
(defgeneric __insert-4 (container arg1 arg2 arg3))
(defmacro insert (container &rest args)
  `(,(__make-method-name :insert (1+ (length args))) ,container ,@args))

 

__make-method-name は以前 new のために書いたコードをメソッド展開用にイジった以下のもの。要するに、パラメータ数にあわせて (insert a b c) => (__insert-3 a b c) と展開してくれるわけだ(本当にそこまでやらなければならないのかは自信がないのだけれど)。

(defun __make-method-name (method arg-count)
  (labels ((my-symb (&rest args)
             (values (intern (apply #'onlisp/mkstr args) :cl-stl))))
    (my-symb "__" method "-" arg-count)))

 

話を戻そう。これでもって、set のための insert メソッドの実装は以下のようになる(本筋から外れないためにも、中身は抜いてあります)。便宜上、A B C という名前をつけておこう。

;; returns pair<iterator bool>.
(defmethod __insert-2 ((container set-container) value)    ; A
  (...))

;; returns iterator.
(defmethod __insert-3 ((container set-container)           ; B
                       (itr set-iterator) value)
  (...))

;; always returns nil.
(defmethod __insert-3 ((container set-container)           ; C
                       (itr1 input-iterator) (itr2 input-iterator))
  (...))

 

で、ごく簡単に動作を確認するわけだ。それぞれのオーバーロードがコールされるようにしている。

(let ((sc (stl:new :set (#{0 5 10 15 20} #'<))))
  ;; calls A
  (stl:insert sc 12)
  ;; calls B
  (stl:insert sc (stl:begin sc) 17)
  ;; calls C
  (let ((v (stl:new :vector (#{2 4 6 8}))))
    (stl:insert sc (stl:begin v) (stl:end v)))
  sc)

 

ここまでは問題ない。ちゃんと動作した。しかし、眺めていると違和感が湧いてくる。ある set に、別の set オブジェクトの反復子からなるシーケンスを insert しようとした場合、ちゃんと C がコールされてくれるのだろうか? 4つめのコードを追加してみると、めでたくエラーになりましたとさ。

(let ((sc (stl:new :set (#{0 5 10 15 20} #'<))))
  ;; calls A
  (stl:insert sc 12)
  ;; calls B
  (stl:insert sc (stl:begin sc) 17)
  ;; calls C
  (let ((v (stl:new :vector (#{2 4 6 8}))))
    (stl:insert sc (stl:begin v) (stl:end v)))
  ;; calls C...?
  (let ((w (stl:new :set (#{14 16 18} #'<))))
    (stl:insert sc (stl:begin w) (stl:end w)))    ; ERROR! calls B!
  sc)

 

エラーの理由は、4番目の insert で B のメソッドが呼び出されたからだ。つまり、(stl:end w) の評価結果である set-iterator を挿入しようとし、それをすでにコンテナ内に入っている数値と #'< で比較しようとしてエラーになったというもの。ちなみに、B のメソッドがコールされるのはこの場合完全に正しい。総称関数呼び出しをどのメソッドに解決するかの規則に合致しているのだ。この場合は2つ目のパラメータが set-iterator である時点で B に決まる(はず)。

さて、これをどうやって解決しようか。考えられるのは以下の2つ。

  1. C のメソッドとは別に D を作成し、set-iterator による範囲を受け付ける。そうすれば set-iterator の組みを渡しても B がコールされることはない。
  2. B のメソッドにおいて、(type-of value) が 'set-iterator ならば範囲を受ける別のメソッドに飛ばす。

 

どちらでも動作するだろう。それはわかっている。わからないのは、(主に CLOS の?)作法として、どちらがより一般的なのか、ということだ。ひとまずのところ、自分の直感は 1 の方法にすべきだと言っているが、はて。

 

コメント

project-enigma - 2014/04/14 19:00:00

結局、最後のポイントについては1の方法を取ることに。次は演算子オーバーロードについて考えている。

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

 

このページのタグ

Page tag : Common Lisp

Page tag : STLとその移植

 

 


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