2015-09-29-01-配列要素のアドレス取得からポインタを生成する話 - project-enigma

2015-09-29-01-配列要素のアドレス取得からポインタを生成する話

>> Site top >> weblog >> 月別アーカイブ >> 2015年09月のlog >> 2015-09-29-01-配列要素のアドレス取得からポインタを生成する話

最終更新日付:2015/09/29 08:19:12


配列要素のアドレス取得からポインタを生成する話

2015 年 09 月 29 日

Effective STL のコードを CL-STL で実装する話はちょっとおやすみ。今回は、配列要素の参照に対してアドレス取得するような構文でポインタオブジェクトを生成する、という最近の変更の話だ。

さて、どこから説明したものだろうか。まず、C/C++ で何が起きるか、というところからだな。以下のコードを見て欲しい。

int arr[10];
...
int* p1 = &arr[0];
int* p2 = &arr[3];

 

好き好んでこういう書き方をするかどうかはさておいて、これで配列要素を指すポインタが取得できる。そして、そのポインタが指す int 値は配列の要素なのだから、ポインタをインクリメント/デクリメントすることで要素を移動できる。当たり前の話だな。

問題は、STL の std::vector なんかでも同じ構文で要素のポインタが取得できるということだ。これは STL の機能というよりは、単純に std::vector<int>::operator[] が配列要素の参照を返しているだけで、それに & 演算子を適用しているというかたちなっている。

で、まぁやっぱり Effective STL の話になるのだけれど、「std::vector の反復子がポインタだと決めつけたりしちゃダメだよ」という警告がはっきりと書かれている。標準はそれを保証していないのだ。だからポインタと決めつけてコンパイルに通ったとしても、それに移植性はない。

そこで operator[] と & 演算子を組み合わせる話になる。これなら反復子と無関係な生のポインタを確実に取得できる。そうする必要はあまりないが、C API にポインタを渡すにはそれ以外に方法はない。そりゃそうだ。

‥‥‥とまぁそんなわけで、CL-STL にもそれができなきゃならん。いや、別になくても困らないのだけれど、Effective STL にも載ってることだし、なんか気持ち悪いからやることにしたのだ。書籍のコードを CL-STL で表現する作業は次回以降に譲るとして、今回はこの機能追加を実現するまでの話を少し書いてみようかと思う。

いわゆる演算子オーバーロード的な部分を担当しているのは CL-STL ではなく CL-OPERATOR だ。これは元々 CL-STL の一部分だったが、別ライブラリとして(随分前に)分離された。わかりやすさのために、with-operators マクロを使おう。このマクロの支配下において以下のような記述は、

(with-operators
  &obj
  const_&obj
  &obj[0]
  const_&obj[idx])

 

以下のように展開され、

(SYMBOL-MACROLET ((&OBJ (_& OBJ))
                  (CONST_&OBJ (CONST_& OBJ))
                  (&OBJ[0] (_& OBJ 0))
                  (CONST_&OBJ[IDX] (CONST_& OBJ IDX)))
  &OBJ
  CONST_&OBJ
  &OBJ[0]
  CONST_&OBJ[IDX])

 

最終的な symbol-macrolet 展開により、以下のコードと等価になっていた。コンパイル時点でだ。

(PROGN
  (MAKE-INSTANCE 'FIXED-POINTER
                 :GETTER (LAMBDA () OBJ)
                 :SETTER (LAMBDA (#:NEWVAL6454) (SETQ OBJ #:NEWVAL6454)))
  (MAKE-INSTANCE 'CONST-FIXED-POINTER :GETTER (LAMBDA () OBJ))
  (MAKE-INSTANCE 'VECTOR-POINTER :BUFFER OBJ :INDEX 0)
  (MAKE-INSTANCE 'CONST-VECTOR-POINTER :BUFFER OBJ :INDEX 0))

 

with-operators を除外して話をすると、要するに _&const_& がいずれもマクロであり(これは正確には CL-OVERLOAD によってオーバーロードされたマクロなのだが)、パラメータ数が1であれば固定ポインタ(FIXED-POINTER)を、パラメータ数が2であれば cl:vector とインデックス値と決めつけて可動ポインタ(VECTOR-POINTER)を返すというわけだ。with-operators はこれを構文糖でラップしている。以上が初期実装の話だ。

で、こんな実装になっていると CL-STL のコンテナで似たようなことをさせる余地がない‥‥‥ここに至って、CL-STL の要請によって CL-OPERATOR の仕様を変更するという話になるわけだな。具体的には、CL-OVERLOAD によってオーバーロードされる _& および const_& マクロのエントリ部分に手を入れ、パラメータ数が1であれば従来通り fixed-pointermake-instance し、パラメータ数が2であれば総称関数呼び出しに展開されるようにした。その総称関数というのは、新しく導入されることになった operator_& / operator_const& だ。これは常に2引数を取る。結果、前述の例は以下のように展開され、

(SYMBOL-MACROLET ((&OBJ (_& OBJ))
                  (CONST_&OBJ (CONST_& OBJ))
                  (&OBJ[0] (OPERATOR_& OBJ 0))
                  (CONST_&OBJ[IDX] (OPERATOR_CONST& OBJ IDX)))
  &OBJ
  CONST_&OBJ
  &OBJ[0]
  CONST_&OBJ[IDX])

 

最終的な symbol-macrolet 展開により、コンパイル時点で以下のコードと等価になる。

(PROGN
  (MAKE-INSTANCE 'FIXED-POINTER
                 :GETTER (LAMBDA () OBJ)
                 :SETTER (LAMBDA (#:NEWVAL6454) (SETQ OBJ #:NEWVAL6454)))
  (MAKE-INSTANCE 'CONST-FIXED-POINTER :GETTER (LAMBDA () OBJ))
  (OPERATOR_& OBJ 0)
  (OPERATOR_CONST& OBJ IDX))

 

これで、(_& obj 0) という記述や with-operators 配下の &obj[0] みたいな記述は、総称関数呼び出しに展開されることになった。で、CL-OPERATOR はこの総称関数のメソッドとして CL:vector に対する実装を提供し、それによって vector-pointer を返すようにした‥‥‥と。以上、ここまでが CL-OPERATOR の変更に関する話だ。

 

で、ここまで来れば CL-STL 側での対応は難しくない。具体的には、stl:vectorstl:array がこれに対応した。stl:vector では、以下のような感じになる。

* (let ((obj (new stl:vector #{0 1 2 3 4 5 6 7 8 9})))
    (with-operators
        (values &obj[0]
                const_&obj[0]
                (stl:begin obj)
                (stl:cbegin obj)
                (stl:rbegin obj)
                (stl:crbegin obj))))

=> #<VECTOR-POINTER {1006B9FD73}>
   #<CONST-VECTOR-POINTER {1006C3FD73}>
   #<CL-STL:VECTOR-ITERATOR {100693CAB3}>
   #<CL-STL:VECTOR-CONST-ITERATOR {10069D4AB3}>
   #<CL-STL:VECTOR-REVERSE-ITERATOR {1006A6FC43}>
   #<CL-STL:VECTOR-CONST-REVERSE-ITERATOR {1006B07C43}>

 

stl:array もまた同様だ。

* (let ((arr (new stl:array 10 #{0 1 2 3 4 5 6 7 8 9})))
     (with-operators
         (values &arr[0]
                 const_&arr[0]
                 (stl:begin arr)
                 (stl:cbegin arr)
                 (stl:rbegin arr)
                 (stl:crbegin arr))))

=> #<VECTOR-POINTER {1006FC0823}>
   #<CONST-VECTOR-POINTER {1006FC1053}>
   #<CL-STL:ARRAY-ITERATOR {1007054BD3}>
   #<CL-STL:ARRAY-CONST-ITERATOR {10070ECF73}>
   #<CL-STL:ARRAY-REVERSE-ITERATOR {1007187D73}>
   #<CL-STL:ARRAY-CONST-REVERSE-ITERATOR {100721FC43}>

 

実際問題として、stl:vector-iterator なんかは vector-pointer から派生しているクラスなので、その意味では、なんていうか、「ポインタを取得できるようにする」ことに意味はない。それどころか、そもそも Common Lisp にはプリミティブなデータ型としてのポインタは存在しないのだし、上記の vector-pointer 自体が「ポインタのように振る舞うべく作成されたクラス」なのだから言ってしまえばイテレータなのだ。そう考えるとワケがワカラナクなってくるので、深く考えるのはやめよう。

 

ひとまずのところ、今回書きたかったコトはここまで。ここから先はちょっと別の話になる。それは、「[] 演算子をサポートする他のコンテナでも、& と併用する記法でポインタを返すようにするべきか?」というモノだ。

ひとまずの対象は dequemap だ。もちろん、これらのインスタンスに対して &obj[0] としても、可動ポインタを返してあげることはできない。固定ポインタがせいぜいだろう。その対応をするかどうか、で悩んでいたところ、自分でも思いがけないことに気付いた。with-operators による構文糖を除けば、現状でもそれなりに動作するのだ。以下のように。

(let ((obj (new stl:map #'<)))
  (with-operators
    (setf obj[2] :two)
    (let ((ptr (_& obj[2])))
      (setf *ptr :zwei)
      (values *ptr obj[2]))))

=> :ZWEI
   :ZWEI

 

冷静に考えれば、それほど驚くようなことでもない。以下のように展開されるからだ。

(LET ((OBJ (NEW CL-STL:MAP #'<)))
  (FUNCALL #'(SETF OPERATOR_[]) :TWO OBJ 2)
  (LET ((PTR (MAKE-INSTANCE 'FIXED-POINTER
                            :GETTER (LAMBDA () (OPERATOR_[] OBJ 2))
                            :SETTER (LAMBDA (NEWVAL)
                                      (FUNCALL #'(SETF OPERATOR_[]) NEWVAL OBJ 2)))))
    (FUNCALL #'(SETF OPERATOR_*) :ZWEI PTR)
    (VALUES (OPERATOR_* PTR)
            (OPERATOR_[] OBJ 2))))

 

となると、構文糖としての &deque[0] みたいなのだけができないのは気持ち悪いということになるので、やっぱりやろうかな。

 

コメント

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

 

このページのタグ

Page tag : STLとその移植

Page tag : Common Lisp

 

 


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