見出し画像

Retro-gaming and so on

GLib入門 GSList編 その1

今回はGLibの肝、と言うか「データ構造とアルゴリズム」と言う「プログラミングの授業」でのメインディッシュである、片方向連結リスト(singly linked list)、を扱う。
片方向連結リストはデータオブジェクトの華形だ。宝塚スターだ。
GLibだろうと、「データ構造とアルゴリズム」の授業だろうと、片方向連結リストさえ手に入れれば他は要らん、ってほど重要なデータオブジェクトとなる。
また、この片方向連結リストが理論的には(完全にそのものではないが)Lispのリストの基礎になっている。

片方向連結リスト

片方向連結リストは2つのポインタ変数をまとめたオブジェクトを「単位」とする。2つのポインタのうち、片方は別のデータオブジェクトを指し、もう一方は同じ「片方向連結リストの単位」を指す(※1)。
この単純な構図を連鎖させる事によってデータオブジェクト「リスト」を得る。
そして、リストの終端は必ずNULLを指す。
これも「配列と言うデータ構造がない」C言語のような言語には驚きだ。「配列がない」と言う事は剥き出しのメモリしかない。剥き出しのメモリしかない、と言う事は終端が存在しない。いや、物理的な意味ではあるけど、プログラム上、データ構造の「終端が分からない」と言うのはそれだけで厄介なんだ。
片方向連結リストはそういう「メモリそのものを扱うのがメンド臭い」と言う諸処の難題を解決してくれるわけだ。ついでに一般的な「配列」とも違って伸長可能だ(※2)。すげぇ。
そして、ここで「データオブジェクトを指す」為に「ポインタ」が出てくる。
C言語脳だと

「だから見ろ、ポインタは大事だろ。C言語でポインタを扱う事が重要なんだ。C言語じゃないと連結リストは作れない。」

とか言い出すだろうが、ハッキリ言おう。むしろ大事なのは構造体と言う考え方、なんだ。ポインタも重要だが、どっちかっつーとサブの役割だ。
そして別にC言語が連結リストを発明したわけでもない。LispはC言語より歴史が長いし、Lispはその初期には機械語で直接作られていた。そしてそれはLispがリストを発明した、って事さえ意味していない。
「連結リスト」はそもそも、Lisp以前にIPLと言うアセンブリ言語の一種によって導入された。連結リストの歴史はC言語より長いんだ。
C言語脳はほっとけば碌な事を言わないし、すぐにトンデモしか言い出さないんで、敢えてここで釘を刺しておく。
ところで、特に民生機なんだけど、70年代後半から80年代初期にかけて、圧倒的に人気があったプログラミング言語はご存知BASICだった。
当時のBASICは数値と文字列と配列くらいしかデータが無かった。BASICでプログラミングしてた人はこの、たった3つのデータ「のみ」でプログラミングしてたわけだ。
とは言っても、これを聞いても「まぁ、そうじゃない?」って言う人が殆どだろう。うん、殆どなんだ。
ところが80年代初頭、PCに導入されたプログラミング言語Pascal。この言語の登場は当時の民生機プログラマに衝撃を与えた(※3)。
一体Pascalの何がその当時の民生機プログラマ(殆どがBASICかアセンブリのプログラマだ)に衝撃を与えたんだろうか。それはPascalが、民生機用プログラミング言語として初めて「ユーザー定義型」を導入したから、だ。「構造体」(Pascalでは「レコード型」と呼ぶ)の導入で「ユーザーがビルトインデータ型だけじゃなくって自分で作ったデータ型でさえプログラミング出来る」と言うメッセージを送った(※4)。
これに、それまで「数値と文字列と配列」しか使ってこなかった(と言うより使えなかった)民生機のプログラマが衝撃を受けるわけだ。「自分で新しく型を定義してエエんか」と。
今じゃ当たり前かもしれないけど、当時の民生機ユーザーには相当デカいパラダイム変換だったんだ。「型さえもプログラム可能」だと言うのは。
ただ、とは言っても「じゃあどんな型をプログラムすりゃあエエねん」ってのは当然出てくるよな(笑)。
そこで、Pascalの教科書では通常、レコード型とポインタを使った実例で、この「片方向連結リスト」と「二分木」を取り上げる事となる。Pascalの作者、ニクラウス・ヴィルト博士自らが書いた著書「アルゴリズム + データ構造 = プログラム」が売れまくった事もあり、在野の民生機プログラマはPascalで初めて「片方向連結リスト」と「二分木」と言うデータ構造に遭遇したわけだ。

 
と言うわけで、別にC言語が密接に片方向連結リストと関係があったわけじゃない。むしろ、長い間、そのテの「アルゴリズム教育」はPascalで行われてたわけだ。
90年代前半までの大学のコンピュータサイエンスでは、まずはPascalを学び、Pascalで「アルゴリズムとデータ構造」を学び、それからOSの実装法を学ぶ為、「実際にソースコードが読めるUNIX」を読解する為Cを学ぶ。
多分この時期の教育法の方が今よかある意味マシだったんだよ。この時代は「Cはプログラミングの基礎」なんつートンデモを言い出すC言語脳が出てこなかった。そして「Pascalを学んでた方が」Cの奇っ怪な文法に、初見で煩わせされる、って事も無かったわけだ。何故なら「Pascalでポインタを既に学んでたから」だ。同じ概念を別の言語で学ぶ方がラクに決まってる。
いずれにせよ、片方向連結リストの肝は、「たった2つのポインタ変数を持った」構造体をデザインする事。メインは構造体、ポインタはサブ、だ。しかし「他のオブジェクトと自分と言う"データ型"を指せる」ポインタが初めて重宝される場面、と言う事ではある。
ポインタはサブでも重要だけど、「C言語のポインタ」及び「その文法」が重要ではない、と言う事だ。

さて、GLibでの片方向連結リスト、と言うデータオブジェクトはGSListとして定義されている。
GSListとはこういう定義だ。



間違えた(笑)。GSって言っても「グループサウンズ」ではない(謎
こっちだ。



構造体が定義されてるけど、おっそろしく簡単な定義だ。gpointerと言う型は実体はvoid*型のポインタで、これは実は「型を指定せず」アドレスだけを知らせる、と言う意味だ。「型を指定せず」と言う事は、結果、「プログラマが最終的に型を指定しないと」いけないんだけど、言い換えると「どんな型を対象にしても指せる」って事だ。つまり、指す相手は整数、実数、虚数、文字配列、文字列、はたまた構造体でも何でもいい。
このお陰で、GSListのdataフィールド/スロットは、実体化する際にプログラマ側が「どんな型なのか」改めて指定する必要があるにせよ、あらゆる値を「格納」出来るように見えるわけ、だ。
そしてnextと言うポインタ変数で、「別のGSList」の先頭アドレスを指すことが出来る。これがGSList同士のリンクを成立させている。

まずは簡単なGSListの使い方を見てみよう。例によって、「世界で一番有名なプログラム」を書いてみる。



これを実行すると、

The first item is 'Hello, world!'

と表示されるだろう。
このプログラムは、RacketのようなLispだと、

(define lst (cons "Hello, world!" '()))
(printf "The first item is '~a'~%" (car lst))

みたいな意味になる。
まず、GSListに於いて、NULLと言うのは直感的には「空リスト」と同義だ。具体的には、GSListのnextフィールド/スロットがNULLアドレスを指している、って事だ。NULLアドレスだけは「型」と関係なく、どんな型のポインタでも指せるアドレス、となっている。
だから、GSListを初期化する際にはNULLを使えばいい、って事だが、g_slist_prependを使えば一気にリストを生成する事が可能なわけだ。
g_slist_prependはLispのconsと引数順序が違うが、コンセプト的にはほぼ同義の関数となる。第一引数にGSListを取り、第二引数に「リストに格納したい」データを指定する。
このプログラムの構造自体は簡単だ。ただし、問題なのはむしろ「出力指定」だ。
ここだよな。

g_print("The first item is '%s'\n", ((GString *)list->data)->str);

いや、g_print自体に問題はない。そっちじゃなくってフォーマット文字列に値を与えてる方が問題なんだ。

((GString *)list->data)->str

これは一体どういう表記なのか、と。
前回、GStringは直接g_printに渡せない、と書いた。GStringのstrフィールド/スロットの値を渡さなアカン、と。
今回はGSListのdataフィールド/スロットにGStringを渡したわけだが、GSListのdataフィールド/スロットはどんな型のアドレスでも指せる、と言う話を上で書いた。つまり、dataフィールド/スロットは指してるアドレスがGStringに関連した「型」なのかどうか分からんわけだ。
結果、ここからGStringのstrと言う実体を引きずり出すには、

  1. GSListのdataフィールド/スロットが指してるアドレスの型を「GString *」型だ、と確定させる。
  2. 1. からstrフィールド/スロットの値を取り出す。
と言う2段階の操作が必要なんだ。
それが

((GString *)list->data)->str

で表されてるんだけど、実はこれがクッソ読みづらいし書きづらい。
そもそも(GString *)がまずは何なんだ、何の作用があるんだ、気持ち悪い、んだけど、これが実は「宣言のように見える」んだけど、単なるキャスト、つまり「型変換」なんだ。ここでlist->dataのvoid *と言う型をGString *型に変えてる、って事だ。
このCの、「なんかのデータ」の前に括弧に包まれた「型名」を於いて型変換、ってのは非常に紛らわしいと思う。Lispのような(変換関数 データ)のような書き方の方がわかりやすくないか。
Rustなんかもこの辺の記法を改良してて、データ as 新しい型、と言う方式の記述にしたのはありがたい(一方、JavaなんかはC言語型の「クソ表記」を受け継いでいる)。
C言語の場合、括弧に別々の意味がありすぎて、それがまず物事をクソややこしくしてるんだ(※5)。
そして(GString *)list->dataと言う表現だが、listをGString *型に変換してるのではなく、list->dataをGString *型に変換している。と言うのも、アロー演算子の方が優先度が高い。つまり->の方が先に演算されているんだ。
この辺のCの文法、と言うか、「演算子」の優先順位が非常に気持ち悪い(笑)。実際、C言語の解説書には必ずと言って良い程「演算子優先順位表」が入っている。これがある理由は「直感的に見るとおかしな演算子の優先順位がある」と告白してるに等しい。実際Lispなんかでそんな表は見たことがない。要はCの演算子の優先順位は「直感に反してて一貫してない」と言う意味なんだ。
これがCのポインタ周りで「あれ?」ってなる原因の一つだ。繰り返すがポインタが難しいのではなく、この辺の文法がCはメチャクチャなだけなんだ。
しょーがないから慣れよう。この単純な、GSListによるHello Worldプログラム「だけ」で、アロー演算子、ポインタ型への型変換、と言うCの諸処の問題が全て詰まってる程だ。ここでポインタ周りに慣れておけば、Cの(クソな)ポインタ周りの記述やその読解法に慣れるだろう。
なお、最初の変数listの宣言で、型がGSList*になってる、ってのもGStringで見た通りだ。片方向連結リストは合成データ(Compound Data)だ。つまり、合成データである以上、先頭のアドレスしか取ってこれない。だからlistをポインタ変数にしないと代入出来ない。
また、GSListも使い終わったらg_list_freeで使用メモリ領域を解放せねばならない。

次に扱うプログラムも簡単なプログラムだ。


これはRacketみたいなLispで言うと次のようなプログラムだ。

(define lst (cons "second" '()))
(set! lst (cons "first" lst))
(printf "The list is now is ~a items long~%" (length lst))

まずはlistを定義して、それに対して再代入を行っている。そしてそのlistの長さをg_slist_lengthで測っている。
これはある意味驚きだろう。C言語の「配列」には長さが無かった。と言うより、原理的に長さを測る手段が存在しない。
一方、片方向連結リストは「長さ」がある。これは「C言語で扱えるデータ型」としては格段の進歩だ。片方向連結リストは圧倒的に「扱いやすい」データ構造なんだ。
もう一つGSListのg_slist_prependには特徴がある。list変数に再代入しなければならない、と言うのはどういう事だろう。

  1. g_slist_prependには返り値がある。
  2. g_slist_prepend自体には変数listを変更する力がない。つまり、この関数は非破壊的だ。
もし、g_slist_prependが変数listを破壊的変更してたら、再代入は必要ないんだ。まるでPythonのappendメソッドみたいに。
つまり、実は上のコードはこう書いても良い、って事になる。


まぁ、一行がクソ長くなる欠点はあるが、非破壊的にプログラムを組める可能性がある、って事は大きい。
上のプログラムはRacketのようなLispでは次のような意味になる。

(define lst (cons "first" (cons "second" '())))
(printf "The list is now is ~a items long~%" (length lst))

次はあまり好きじゃない機能に付いて書く。g_slist_removeだ。
これも非破壊的関数で、指定した要素を削除したGSListを返す関数なんだが、上のコードをこう改造しても上手い具合に動かない。



g_string_new("first")と指定したオブジェクトを削除しようとするが上手く行かず、相変わらずリストの長さは「2」と表示されるだろう。
これの理由は前回書いた、g_string_newの性質による。g_string_newは常に新しいオブジェクトを生成して返す。つまり、g_string_new("first")g_string_new("first")なんだ。
一方、g_slist_removeはあくまで指定されたオブジェクトと「同一の」オブジェクトを削除する。
これじゃあ上手く動くわけがない。
従って、こういったケースではGString *型のオブジェクトを別に定義しておかないとならない。


しかし、もうちょっとツッコんで考えてみよう。要は「この世で一番リストを使っている」Lisp使いがこんなコードを書くだろうか。
いや、それ以前に、Lispにはconsはあるが「要素を削除する」関数が存在しないんだ。
このテのお題をLisp使いが受け取った時、彼らは次のようなコードを書き、破壊的変更を招きそうな状態を避ける。

(define lst (cons "first" (cons "second" '())))
(printf "The list is now is ~a items long~%" (length (cdr lst)))

そう、単にリストのcdrを取る。
つまり、このテのお題だと、次のように書いた方が個人的にはスマートだと思う。


そう、list->nextを取れば済む話だ。
よって、個人的にはg_slist_removeは封印しようと思ってる(笑)。強制はしないが(笑)。

一方、Schemeの仕様には存在しないが、リストから特定の要素を全削除する、ANSI Common Lispのremoveのような関数が欲しい場合がある。
GLibで、それにあたるのがg_slist_remove_allだ。
次のようなコードを見てみよう。


g_slist_remove_allも非破壊的な関数で、指定したオブジェクトを取り除いたGSListを返してくれる。上の実行例だとthirdと言うデータを取り除いたリストを返してくるんで、表示は3となる筈だ。
上の例だと、firstsecondthirdと言う文字列オブジェクトを生成したんで、それぞれメモリを解放せなアカン、と言う面倒くさい事になっている。あとで、それに対するもうちょっとマシな解決策を提示しよう。
なお、上のコードはRacketで言うと次のような意味だ。

(define 1st "first")
(define 2nd "second")
(define 3rd "third")
(define lst (cons 1st
       (cons 2nd
         (cons 2nd
           (cons 3rd
             (cons 3rd '()))))))
(printf "The list is now ~a items long~%" (length (remove* `(,3rd) lst string=?)))

次は、Schemeで言うlist-refにあたる関数、g_slist_nth_dataだ。



これは使い方が簡単だろう。
GSListはリストのn番目のデータを検索する、g_slist_nth系の関数がいくつかあるんで(g_slist_lastg_slist_nextg_slist_nth等)、自分で使い勝手を調べてみて欲しい。
なお、上のコードはRacketのようなLispだと次のようなコードになる。

(define lst (cons "first"
       (cons "second"
          (cons "third" '()))))
(printf "The first item is '~a'~%" (list-ref lst 0))
(printf "The item at index '1' is '~a'~%" (list-ref lst 1))
(printf "And the last item is '~a'~%" (list-ref lst (sub1 (length lst))))

次は自作の構造体を使ったデータ型を利用する例だ。



取り敢えず良くある例として、構造体でPerson型、と言うデータ型を作る。
通常、構造体を受け取る変数(ポインタ変数)を宣言する場合、mallocを利用して必要なメモリを確保するが、「書式がメンドイ」と言うのは以前見たと思う。
一方、GLibではPascal的な「動的変数」を確保出来るg_newと言う手続き(実際はマクロ)が用意されている。
このテの手続きはJavaなんかのコンストラクタでもお馴染みだろう。これにより、GLibはmallocに纏わる書式の面倒くささを回避してくれている。
あとはこのコードに於いては特筆すべき事はないが、Fredに纏わる印字部分、

 g_print("The last Person's name is '%s'\n"
    , ((GString *)((Person *)g_slist_last(list)->data)->name)->str);

で迷わないように。例のC特有の「型変換」記述のメンド臭さがモロに出てるが、「冷静に」書いていけば何とかなる筈だ。
ならなかったら「C言語の演算子の優先順位はクソ」と500回くらい唱える事。そうすれば解決せんでも気は晴れるだろう(笑)。
また、構造体を使ったデータ型、tomfredのメモリを解放する必要があるが、GLibを使ってる最中では、C言語標準のfreeを使わずにGLibで用意している江川達也のデビュー作のような、g_freeを用いるべきだ。

次はGListの結合や反転だ。次のコードを見て欲しい。


GSList同士の結合はg_slist_concat、GSListの反転はg_slist_reverseで行う。
この2つともやはり非破壊的な関数で、返り値としてGSListを返す。
しかし、C言語脳はとかく破壊的変更を行おうとするのに、さすがC言語のエキスパート達が作り上げたライブラリだけあって、不用意な破壊的変更はしないようだ。
ただし、上のコードだと「リスト結合」が二回行われてるんで(これらが「非破壊的」なんでそれを見せる為に敢えてそうしたが)、実際は「リスト結合」に関しては新しくGSListを作ってそれに代入した方が良いだろう。
これも「再計算」を防ぐ為だ。



生成した全てのGSListデータを破棄(メモリを解放)するのを忘れないようにしよう。
なお、上のコードをRacketで書くとこんなカンジだ。

(define lst1 (cons "first" (cons "second" '())))
(define lst2 (cons "third" (cons "fourth" '())))
(define both (append lst1 lst2))
(printf "The third item in the concatenated list is '~a'~%" (list-ref both 2))
(printf "The first item in the reversed list is '~a'~%" (car (reverse both)))

次は、GSList用に定義されてるイテレータを扱う。次の例を見てみよう。



g_slist_foreachはSchemeで言うトコのfor-eachにあたるイテレータだ。返り値を持たず、破壊的変更を含む、副作用目的で使うイテレータ、となる。
定義は次のようになっている。



g_slist_foreachの第一引数は操作対象のGSList、第二引数にコールバック関数を取る。上のコード例だと、まずは出力関数としてのコールバック用関数、print_iteratorprint_iterator_shortの二種類の関数を例として作っている。
そして第三引数として、コールバック関数の第二引数に与えるデータを与える事になっている。
これが指し示す事は、「ラムダ式が無い」C言語上で、GLIBのg_slist_foreachはコールバック関数として最大で2つ引数を取る関数を想定してる、って事だ。仮に、コールバック関数が引数が一つしかない場合、g_slist_foreachの第三引数にはNULLを与える、と言う事になってるようだ。
そんなわけで、例示の為に、上のコードでは2つ引数が必要なprint_iteratorと1つしか引数がないprint_iterator_shortを作ってる。
そして両者共、第一引数としてリストの要素を取る前提になっている。そう、いずれにせよ、コールバック関数の第一引数はリストの中身が1つづつ手渡される。
そして、g_slist_foreachの第二引数へ渡すコールバック関数は、どうやらvoid型の関数、つまりプロシージャじゃないと駄目なようだ。まあ、これはg_slist_foreachが副作用目的なんでそのまんま、だろう。そしてそれらプロシージャをGFunc型へとキャスティングしなきゃならない(※6)。
さて、上のコード例だと「敢えて」GString *型として変数firstsecondthirdを定義している。
それはGSListに格納してたデータがこのように「別々に」定義されてた場合、メモリ解放に纏わる面倒くささがあるから、だ。
今までだと個別に、例えばこういうケースだとg_string_freeで解放してた。
一方、GSListに格納(っつーかdataポインタが指してる)されてる場合、メモリ解放手続きをg_slist_foreachでイテレートしながら使う事が可能だ。
この部分だよな。

g_slist_foreach(list, (GFunc)g_string_free, NULL);

こうすれば、GSListに格納されたデータオブジェクト自体も比較的簡単に解放できる(※7)。
ただし、GSList「自体」のメモリ解放をしてるわけじゃないんで、改めてGSListのメモリ領域は解放しないとならない。
なお、上のコードはRacketだと

(define lst (cons "first"
        (cons "second"
           (cons "third" '()))))
(printf "Iterating with a function:~%")
(for-each (lambda (x)
     (printf "--> ~a~%" x)) lst)
(printf "Iteration with a shorter function:~%")
(for-each displayln lst)

みたいな意味だ。だんだん、やっぱ「同じ事をやってもLispの方が短く書ける」って差がハッキリ出てきている。
やはり「関数がファーストクラスオブジェクト」であり、「ラムダ式がある」と言うのは大きいんだ。

さて、こうやってやっとこさ、LispやPythonで言う「高階関数」、っつーか「高階関数モドキ」が出て来たが。
生憎、さすがのGLibでもmapは提供してねぇんだよな。
恐らくそれをやっちゃうと、「データ型を提供する」と言う基礎目標を遥かに超えてしまう、と言う事。また、実装が、「副作用目的」のforeachに比べると煩わしいんだろう。
所詮C言語は、UNIX的プログラミングスタイルの理想はともかく、機械語代わりの「破壊的変更Welcome」言語、ってのがどうしても設計上の基本だ。そこで頑張ってmapを実装して提供するのもライブラリの枠をはみ出る、って事なんだろう。
従って、基本的にはLispでmapを使いたいような局面ではここで見せたようなforループのテクニックを使わないとならない。

例えば、Schemeで言う

(map + '(1 2 3) '(4 5 6))

なんかのコードは実はGLibでもかなり厄介なんだ。
こんなカンジのコードになる。



まず、GSListは「どんなデータも指せる」と言った。
ただし、それは原則「構造体で作られるデータ」で、一方、C言語のプリミティヴな、整数とか、浮動小数点数とか、そう言うのは指せない、っつーか指しづらいんだ。
なんせ、GSListはポインタ塗れが前提なんだけど、プリミティヴな型は「ポインタ前提」じゃない。
だからLispみたいに「数値をツッコんだリスト」のような「基礎的なリスト」に対して、一体どうやれば作成出来るだかサッパリ分からないんだよ(笑)。まともなチュートリアルがねぇから(苦笑)。
んで、GLibには変換マクロ(Conversion Macros)ってのがあって、整数を使いたい時、メモリを確保してそこに整数をぶち込み、ポインタを得るようにしてくれるマクロ(GINT_TO_POINTER)、そしてその逆変換マクロ(GPOINTER_TO_INT)がある、と。
それを使いつつ「整数のリスト」を生成、そして空リストを持つGSList、iteratorを作って計算結果(この記述がまたややこしいが・苦笑)をg_slist_appendしていく(※8)。
とにかく、「Lispじゃ簡単に扱える整数のリスト操作」が「型変換塗れ」で大変な事になってる、ってのが分かるだろう(※9)。
このように、C言語では「計算ロジックと全く関係ない」事柄に対して非常に手間がかかるワケだ。GLibは物事を易しくしてくれるが、それでも「C言語の本質」を全て緩和してくれるわけでもないんだ。

結局、例えばLispやSchemeで

(map (lambda (x) (* x x)) '(1 2 3))

とたった一行で済む計算も、C + GLibなら


とならざるを得ないんだ。
GLibは役に立たないと思った?
でもGLibナシならもっと量の多いコードになってるのは間違いないんだよ。

続く。

※1: 「片方向連結リストの単位」とは随分と長ったらしい名称だが、暫定的、だ。
と言うのも、「それ」を何と呼ぶのか、ってのは一般的な名称がないんだ。
なお、Lispではこの「単位」をコンスセル、と呼称する。constructor cellで、古いLispの本なんかでは「コンス細胞」等とも訳された。

※2: 最近のプログラミング言語だと「配列」と名付けられてても可変長、ってのが多くなってきたが、元々「配列」とは宣言時に長さを指定しておく「固定長」が多かった。

※3: 特に衝撃を受けたのがAppleの社員だったらしい。Apple IIでのPascalが「あまりにも素晴らしかったので」Appleのプログラマのその殆どがPascal信者になり、後のApple MacintoshのOSの開発言語としてPascalの拡張版、Object Pascalを採用する事となる。
Object Pascalが現iMacが出てくるまで、Mac OSの開発用言語となった。

※4: 厳密に言うと、「構造体」及び「レコード型」がそのまま「データ型」を生成するわけじゃない。この辺、オブジェクト指向言語のプログラマやLispなんかのプログラマは誤解しやすいが、一般に、構造体/レコード型は「名前で要素にアクセス出来る特殊な配列」を提供するに過ぎない。
重要なのは、それに「型名を付けられる」機能で、これにより、構造体/レコード型を「データオブジェクト」として再利用出来る、ってのがコンセプトだったわけだ。
C言語では「既存のデータ(つまり構造体を含む)」に新しい型名を与える機能がtypedefで、Pascalでは型宣言部でのtype宣言により、「そのプログラム内で新しく使う」データオブジェクトの型名を定義出来る。

※5: 実はC言語では括弧そのものと(型名)、つまりキャスティング(型変換)演算子は全く違う機能だ。紛らわしい。

※6: そもそも、「関数がファーストクラスオブジェクトじゃない」C言語だと、LispやPython等の「モダンな高級言語」のような高階関数が作れない。代替手段として「関数ポインタ」なるものがあり、Lisp/Pythonのような高階関数をエミュレートするには、コールバック関数として「関数ポインタ」を渡すしかないわけだ。
GFuncと言うのは、その「関数ポインタ」のGLibなりの抽象化だ。

※7: ただし、ちと自信ナシ。と言うのも、g_string_freeは二引数関数で、第二引数があり、TRUEを与えるのが基本戦略な筈なんだけど、このコードはそれを無視してる。無視してる割には「上手く動いてる」わけだ。
実は色々と試してみたんだけど、結果「上手く動いてるらしい」と言う結論になったが、「何故に?」ってのは分かんなかった(笑)。ごめん(笑)。
そもそもg_slist_foreachの仕様上、第3引数はポインタ型じゃなきゃいけないんだけど、TRUEを無理矢理ポインタ化して与えても結果変わらず、だったわけで、なんか俺が知らない「マジック」が働いてんのかもしんない(笑)。
いや、言っとくけど、こういうのは「理論」じゃねぇからな(笑)。人が不格好に作ったモノに対して(C言語だ・笑)、「こうこう考えればこうなる」ってのは「ヒネた考え方」ではあっても、ストレートに「理論」って程の話じゃない。
むしろ「解読するのが厄介な」ブツを作った、って事を反省してくれ。

※8: g_slist_appendg_slist_prependと違って「リストのケツの方から」要素を追加していく。Lisp的には「逆cons」に見える関数だ。
ただし、計算速度はg_slist_prependに劣る。
と言うのも、g_slist_prependは既存のリストの先頭を指せばいいが、GSListの「ホントのケツには」NULLがある。
要は、NULLを避けつつ、その前の要素とNULLの「連結」を「つなぎ変えなきゃならない」辺りにコストが生じるわけだ。
実質上、g_slist_appendは「挿入」と言うちとコストが高い事を行っている。
ただ、このコードの場合、g_slist_prependで繋げていって、g_slist_reverseで結果のGSList、iteratorをひっくり返す、って手順が面倒だったんで(これはこれでコストがかかる)、g_slist_appendを使った。

※9: この辺実は自信なし。ただ、ネットで検索した断片的な情報を統合すると、この型変換マクロを使うしか手がないだろう、って事だ。
もっと上手い手がある、ってぇのなら、誰かに教わりたいモンだ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事