一応「GLib入門」なる記事を書くのは終わったつもりなんだけど、g_autoptrの使い方、ってのがある程度わかったんで、追加記事を書く事にする。
言わばおまけだ。
その前に、C言語にあまり明るくない人の為に「ポインタ」と言う概念とそれに纏わる話をちと書いておこう。
例えばLispで次のようなリストを作るとする。
> (define lst '(a (b c) d))
> lst
'(a (b c) d)
>
僕らはLispで何気なく簡単にリストを作る事が出来るが、実はこれは内部的には次のようになっている。
Lispではメモリ領域のある量を1単位として、それを2つ繋げたものをLispプログラム上の単位としている。これをコンスセルとかそのまんまペアと呼んでる。そして向かってコンスセルの左側をcar、コンスセルの右側をcdrと呼ぶ。
コンスセルが持ってるのはメモリの別の場所のアドレスだ。上の図だと、矢印がそのアドレスが「指してる」場所を表している。抽象的に、この矢印をポインタと呼ぶわけだ。
上の例だと、シンボルlstはaを指してるcarを持つコンスセルを指し、そのコンスセルのcdrは次のコンスセルを指し・・・と連鎖してる事を示してる。
Lispは言わばポインタ塗れの言語なんだ。一方、ユーザーは、そういう概念があるな、と知ってはいてもポインタをそこまで意識する必要もない。
なお、コンスセル自体は単純には、C言語では次のような構造体を使った定義になるだろう。
carもcdrも「どんな型のデータだろうと」指せなければならない。よって、特定の型データを指すわけではない万能のポインタ、voidポインタ型のスロットとしてcarとcdrを宣言している。言い換えると、スロットcarとスロットcdrには「型を問わない」一般的なアドレスが代入される、と見ていい。
さて、まぁ上の話はある意味復習なんだけど、例えばLispで続いて次の操作をするとする。
> (set! lst (cons (car lst) (cddr lst)))
> lst
'(a d)
そうすると、シンボルlstが持つ連結は次のように変更されるだろう。
carがaを指すコンスセルのcdrはcarがdを指すコンスセルを指すように変更され、真ん中にあるコンスセルはどこからも指されなくなる。シンボルlstは相変わらずcarがaを指すコンスセルを指してるが、真ん中のコンスセルはどうにも指定しようがなくなったわけだ。連結が切れたせいだな。
こういう風に「どこからも指されなくなった」コンスセル、言い換えるとメモリ領域は「そこに残ってはいる」んだけど、どうにも使いようがない。単にメモリ領域を専有しちゃったままで、使えるメモリの総量を減らすだけ、の存在と成り果てるわけだ。これを通称ゴミ(ガベージ)と呼ぶ。
そう、Lisp及びPythonやJavaに搭載されているGC(ガベージコレクタ)とは、このゴミを発見して、「このメモリ領域は潰しちゃって再利用していいよ」と言語処理系あるいはOSに伝える機能なんだ。また、その作業を「メモリを解放する」と言う。
これがガベージコレクタの役目、なんだけど、C言語のようにガベージコレクタが無い言語の場合、手作業でメモリを解放しなきゃならない。これがプログラマのミスを誘い、よくある、「あるソフトウェアを長時間起動してると反応が悪くなったり固まったりする」みたいなバグを誘発するわけだ。これをメモリリークと呼ぶ。
このように、手作業でメモリを解放する、ってのは思ったより難しい。と言うか忘れがちなんだ。このテのポカは「するもんだ」って思ってた方がいい。
一方、ガベージコレクタ、と言うのは一般に「重い」と思われている。と言うのも、逐次ゴミ回収に回る、ってのもソフトウェアのパフォーマンスを低下させやすい。結果、ある程度はゴミの存在をほおっておいて、ある程度溜まったら「一気に回収する」と言うのがよく行われるわけだが、と言う事はソフトウェア実行中に「一旦実行を中止して」ゴミ回収せねばならない、と言う事になる(※1)。昨今のコンピュータは速いんで、「ゴミ回収の為に一旦ソフトウェアを停止させ」ても特に体感的には影響を感じづらくなってはいるが、やはりこれはこれでパフォーマンスの低下を招き、これを嫌っているプログラマも結構いるわけだ。
繰り返すが「メモリの解放」は悩ましい問題だ。GCを導入すればプログラマ側はラクだけどソフトウェアのパフォーマンスを低下させる。かと言って人力でやるとメモリリークを招きやすい。
そんな中で、GCに代わって「メモリの自動解放」が何とかならんか、と言う研究が出てきて、GLibに搭載されている一つの解が、g_autoptrと言うマクロだ。
ところで、g_autoptrでググると、どういうわけかC++のauto_ptrが引っかかるだろう。名前がほぼ同じなんで同じモノなのか、って思いがちだが、この2つは違う。と言うか、結論から言うとC++のauto_ptrの方が高機能だ(※2)。
C++のauto_ptrなんかをスマートポインタと呼び、それは半GC的な動きをするが、GLibのg_autoptrとは全く違う仕組みだ。基本的にはマジであるデータに対していくつポインタから指されているかを数え上げ(Reference Count方式)その数が0だった時に自動でメモリを解放するわけだ。また、データに対しての変数が、そのデータに関してのメモリ解放の権限を持つか否か、の概念があり、それを所有権と称する。
そう、Rustの所有権とはここから来てるんだろう。言っちゃえばRustは、このスマートポインタ前提で組み立てられた言語だ、と言う事だ。明示的にスマートポインタを宣言するのではなく、「自然とスマートポインタを含んでる」言語としてデザインされてるんだろう、って事だ(※3)。
しかしながら、GLibのg_autoptrはそこまでスマートじゃない。
あくまでGCC/Clang用なんで、これを使うと「処理系間の移植性が無くなる」って事は覚えておこう。
まぁ、僕はLinuxユーザーなんで関係はないんだけど。
さて、g_autoptrの動作は簡単で、g_autoptrで宣言された変数は、それが定義されている関数がスコープを外れると変数の中身が破棄される、と言う動作になる。
例えば、Racketのnumber->string関数をエミュレートする関数を書くとしよう。
> (number->string 26 2)
"11010"
Racketのnumber->string関数は第一引数で与えられた整数を第二引数を基数とした数値表記に変換する。上の例だと、26を2進数表記すれば11010になる、と言っている。
これをGLibを使って移植しようとすれば、第一ヴァージョンとしては次のコードが考えられる。
関数number2g_stringは内部的にGStringを2つ持っている。変数aは最終的に返り値になり、変数strは剰余計算で与えられた数値zに対して粛々とGStringに変換する為の一種バッファの役目を果たしている。
両者ともC言語のコンパウンドデータなんで、直球勝負ではどっかで「メモリを解放」せなアカンけど、ここではg_autoptrで自動でメモリを解放させよう、っちゅーわけだ。
なお、g_autoptrを使わなかったら
GString*
が型になり、*でポインタ宣言をせなアカンが、g_autoptrはポインタ宣言用のマクロなんで、決してg_autoptr(GString*)等とは書かん事。意図が無ければg_autoptr使用時に*(アスタリスク)は要らない。
さて、ロジックはこれでいいハズなんだけど、生憎下の出力を見ると何も出力されていない。と言うか、基本的に印字されてるのは"\n"と言う改行文字だけ、になってる。
はてさて、これは一体どういう事なのか。
これは単純に言うと、number2g_stringが呼び出され、最終的にaを返した際、number2g_stringと言う関数のスコープを抜けるわけだ。この時点でaのメモリが解放される。
と言う事はaが持ってたデータの内容は全部破棄されるわけだ。破棄された後の内容がg_printに手渡される為、結果、「何も印字されない」と言う事になる。
一方、変数strはnumber2g_stringと言う関数の中でしか使われてない。結果number2g_stringの終了時に外部的には全く影響がなく、無事メモリが解放されるわけだ。
と言う事は、だ。一つの方策としては。
GLibのデータ型を返したい場合はそのデータにg_autoptrを被せてはならない。
と言う事が分かる。返り値がGLibのデータ型だった場合、受け取る側の変数にg_autoptrを使うか、手作業でそれを解放すべきだ、って事だ。
例えば上のコードは、こう改変すれば無事実行される。
上の例だと、g_print内にg_string_freeで囲んだnumber2g_stringを渡してる。第二引数がFALSEだった場合、ガワであるGStringだけが破棄されて、中に入ってる文字配列(この場合"11010")は破棄されない。
従って、この部分は
g_print("%s\n", "11010")
と書くのと同じ効果になる。
あるいは、main関数内でnumber2g_stringの返り値を受け取る変数を設定して、それにg_autoptrを被せてもいいだろう。
この辺は「とにかく変数に代入する」C言語の必然性が垣間見れる例だと思う。
もう一つは、とにかくGLibのデータ型を宣言する際g_autoptrを使い、そのうちの一つを関数の返り値とする場合、g_steal_pointerを利用する事、だ。
平たく言うと、g_steal_pointerで指定された変数はスコープを出る際にg_autoptrによって破棄されない。また、破棄させたくない変数のアドレスをアドレス演算子(&)によって指定する。
これがg_autoptrの使い方、だ。g_free系の関数を使って手作業でメモリを解放するのと、どっちがラクなのか、ってのは正直言ってビミョーだとは思うが、いずれにせよ、これによってある程度自動でメモリを解放する事が出来る。
繰り返すが、関数がGLibのデータ構造を返そうとした場合、その関数のスコープを抜けた際、そのデータ構造は破棄される。returnするデータ構造は、「自動でのメモリ解放」をアテにしないようにしよう。それ以外は自動解放に頼って良い。
と言うわけで、本当にオシマイ。
※1: 単純にはそうだ、って話だが、この「ゴミをどうやって発見するか」と言うのも結構難しい。
例えばあるプログラムが使うメモリ領域を二等分しておいて、片方が満杯に近づくと、現在プログラムが持ってるポインタとそれが指すメモリの中身をもう一方に「全部移動させて」、残ったブツを「ゴミ」と判断する、等と結構複雑な計算を行うガベージコレクタなんかも存在する。
一概に「ガベージコレクタ」と言っても色んな実装方式があって、これはこれで一筋縄とはいかないんだ。
※3: 初期の実験段階のRustはGC付きだったらしい。
※4: どころか、そもそもマイクロソフトの処理系でGLibが使える、なんてのは知らなかったんでビックリした。原因はGLibドキュメントに何も書いてないからだ。
今まで、Gauche以外のSchemeのドキュメントの質の悪さに悩まされてきたが、GLibはそれ以上質が悪い(笑)。例示は0、とかどんなドキュメントなんだ、とか思う(苦笑)。定義ばかりで「どうやって使うのか」と言う観点が欠けている。
ぶっちゃけた話、C++でSTLを検索した方がよっぽどマシなドキュメントが揃っている。
このGLibのドキュメンテーションの質の悪さ、が何故にC言語ユーザーが多い割にはGLibユーザーが少なく、結果日本では(俺の記事を含めて・笑)マトモに言及してる記事が少ない原因じゃなかろうか。