* メモリ管理#6 -(by [[K]], 2022.04.19) ** (1) -C言語はC++言語とは異なり、クラスに対して強制的にコンストラクタやデストラクタが呼び出されるような仕組みがありません。だからプログラム内で明示的に呼び出す必要があります。・・・これにより、呼び出しタイミングの工夫などをする余地が生まれるものの、ほとんどの場合は単に面倒なだけです。これを改善しようと思いました。 -いや、それならそもそもCなんか使わずにC++を使えばいいじゃんって思うかもしれませんが、C++で適当にクラスを使うとなんか専用のランタイムライブラリが追加されるのか、サイズが15KBくらい大きくなってしまいます。私は実行ファイルサイズが小さいプログラムを作るのがすごく好きなので、このよくわからない15KB程度の増加は受け入れられません。それでCだけでプログラムを書きたいと考えました。 ** (2) -私がCでクラスっぽいことをやりたいときは、structで構造体を作って、それでその構造体名で始まる関数をいくつか作ります(ここではその構造体名を仮にClassNameとします)。この関数群はメンバ関数の代用です。ClassName_init()はコンストラクタで、ClassName_deinit()はデストラクタです。 -Cでクラス処理の真似をすると、最初に面倒に感じるのは、デストラクタの呼び出しです。プログラムを書いていると、いつの間にか呼び出すのを忘れてしまうのです。いや、一つか二つくらいだったら間違えないのですが、5個とか10個とかになってくると、抜けが出てくることがあります。・・・しかも、仮にデストラクタを呼び忘れてもたいていはメモリリークを起こす程度で、なかなか発見できません。 -そこで私は、デストラクタを直接呼び出さなくてもいい仕組みを作りました(AClean)。ACleanクラスは、関数呼び出しを登録しておいて、AClean_out()したときに登録順と逆順にそれらをすべて呼び出すクラスです。 ABegin(c1); // ACleanオブジェクトを宣言して初期化するマクロ Test a, b, c; Test_init(&a, c1); // こうすることで、AClean_out(c1)したときにdeinit()が自動で呼ばれるようになる. Test_init(&a, c1); // こうすることで、AClean_out(c1)したときにdeinit()が自動で呼ばれるようになる(そういう記述がinit内に書いてある). Test_init(&b, c1); Test_init(&c, c1); Test_say(&a, "hello"); ... AClean_out(c1); // これでa,b,cはdeinit()される(c,b,aの順に). -mallocしたのにfreeし忘れるというバグも時々あります。だからfreeもAClean_out()に任せられるようにしてあります。 ABegin(c1); char *d = aMalloc(1234, c1); ... AClean_out(c1); // ここでfree(d)もしてくれる. -これならAClean_outだけを忘れないようにすればいいことになります。それなら忘れずにやるのはずっと楽です。 ** (3) -このclean-outの仕組みがあることで、free忘れやデストラクタ呼び出し忘れなどをほぼなくせます。これは気持ちのいいことです。 -しかしまだ防げないものがあります。それは開放タイミングを間違えるというバグです。・・・つまりclean-outしたあとに、またそのオブジェクトを誤って使ってしまうかもしれないわけです。 -でも大丈夫です。そんなポカをやってしまっても、直ちにエラーになって止まってくれます(そういう仕組みを持っています)。だから適当にやっても平気です。もうこのことに気を使う必要はありません。 -それだけではありません。この仕組みはinit忘れも検出します。つまりオブジェクトのポインタとして有効ではないものを使おうとしたらすぐにエラーになるのです。 ** (4) -ACleanのおかげで、オブジェクトの寿命管理がすごく簡単になりました。そこでこんなxsprintf()を考えました。 -これは生成した文字列をmallocしたメモリに返してくれるsprintfです。 char *xsprintf(AClean *c, const char *f, ...) { char s[1024 * 1024]; va_list ap; va_start(ap, f); int l = vsnprintf(s, sizeof s, f, ap); char *t = aMalloc(l + 1, c); strcpy(t, s); va_end(ap); return t; } -このxsprintf()があれば、こんなことができます。 char *s = ""; ABegin(c1); for (i = 0; i < 100; i++) s = xsprintf("%d %s", i, s); puts(s); AClean_out(c1); -これを実行して得られるのはこんな結果です。 99 98 97 96 95 94 93 92 ... 9 8 7 6 5 4 3 2 1 0 -プログラムでは、文字列をメモリ上に遠慮なく作って、そのまま使い捨てにしています。それでもAClean_out()で全部きれいに片付くので問題なしです。 -xsprintf()を使ったプログラムは、sの長さが何バイトくらい必要だとかそういうことを一切気にしていません。freeのことも気にしていません。ただ文字列をぺたぺたとつないでいるだけです。 ** (5) -別の例を紹介します。Matrixという自作のクラスがあって、これはm行n列の行列を表現できます。これを2x2に限定して初期化するMat22_init()があります。 ABegin(c1); Matrix *A = Mat22_init(c1, 1, 2, 3, 4); Matrix *B = Mat22_init(c1, 5, 7, 8, 6); Matrix *C1 = A.add(c1, B).mul(c1, A.sub(c1, B)); // (A+B)*(A-B) Matrix *C2 = A.mul(c1, A).sub(c1, B.mul(c1, B)); // A*A-B*B C1.print("C1="); C2.print("C2="); AClean_out(c1); -ええと、C言語ではメンバ関数は使えません。というかそもそもポインタ変数に対しては->でメンバ関数を書くべきなのですが、上記ではそうなっていません。これはどういうことかというと、D言語のUFCSがあまりにもうらやましくて、C言語で使えるように、トランスコンパイラを書いたのです(実際にはまだ書き途中だけど)。それでついでにポインタ変数に対しても.でメンバ関数を書けるようにしたのです(そのほうが使いやすそうだから)。 --D言語のUFCSというのは、 a.func(b, c, d) を func(a, b, c, d) に解釈してくれる機能です。メンバ関数ではないものをメンバ関数であるかのように書いても許してくれます。 -さらに上記のプログラム内でc1指定が多すぎてうっとうしいので、これを省略可能にしてトランスコンパイラ側で補完させたいと思っています。 -ここでも.add()や.mul()はMatrix *を返しています。オブジェクトをどんどんaMallocして、そのポインタを返しているだけです。 ** (6) -(4)や(5)の例では、オブジェクトの代入は単純なポインタ代入を想定しています。 b = a; とかです。これはメモリ上に作られた値は改変しないという前提で、代入コストを下げることにしたからです。値を改変したいときは、専有しているときに行うか、もしくは新規にオブジェクトを作るようにしています。 -これはC++の基本的な方針とは異なります。C++ではちょうど変数の数だけオブジェクトがあって、一対一で対応しています。ポインタは変えません。一方でJavaは(4)や(5)の例と同じようにしています。 -C++のやり方では、コピーのコストを下げるためにムーブなどの操作を考案していて、ムーブしたらムーブ元はもう値が使えないなどの制約があります。でもJava方式ではそういうことはありません。・・・C++がJava方式を採用できなかったのにはわけがあります。C++のオブジェクト管理方法では、代入元の変数がスコープを抜けてしまうとメモリがfreeされてしまうので、ポインタが無効になってしまうのです。だから単純なポインタ代入では失敗するケースが出てきます。一方でJava方式はJavaにガーベージコレクションがあるので、消されることはないのです。だからこれができるというわけです。 -そして私のAClean方式は、オブジェクトの寿命は変数のスコープとは別になっていて、プログラマが容易にコントロールできるので、Javaと同等の管理が可能です。それで、Java方式を真似ても破綻しないのです。 -ちなみにC++でも参照カウンタを使えば、Javaっぽいことができるようになります。ただしその場合は、循環参照によるメモリリークの可能性はあります(AClean方式なら循環参照問題は生じません)。 ** (7) -先に示したxsprintf()ですが、以下のようにすることもできます。 char *xsprintf2(AClean *c, const char *f, ...) { char s[1024 * 1024]; va_list ap; va_start(ap, f); int l = vsnprintf(s, sizeof s, f, ap); if (strcmp(s, "0") == 0) return "0"; // この行を追加. char *t = aMalloc(l + 1, c); strcpy(t, s); va_end(ap); return t; } -これはたとえば結果が"0"になることが非常に多い場合(かつ結果がcloneされることをあてにしない普通の文脈でしか使わないのなら)、わざわざ結果をmalloc領域にコピーせずに、文字列リテラルを返すことにしようという戦略です。このほうが高速です。 -こういう工夫はC++方式ではできません。C++方式で許されるのは、常にmallocしたメモリを返すか、常に文字リテラルを返すか、もしくはフラグか何かを用意して、どちらを返したか知らせるような、そんな方法しかありません。C++方式では返されたメモリをあとでfreeする必要があるのかないのか、伝えなければいけないからです。 -AClean方式なら、ACleanに登録されるか否かで処理が自動で分けられるので、面倒なことはありません。 ** (8) -セキュリティとの関係について。このACleanはセキュリティを意識して開発したものではありませんでしたが、でも結果的にセキュリティ面でも役立つものになりました。メモリのリークは激減するでしょうし、(3)で書いたチェック機構により、オブジェクトの使い方を間違えることが減っています。 ** (9) -ACleanの仕組みはとても単純です。200行にも満たないものです。それなのに、ここを起点にして私のC言語の使いやすさは大きく変わった気がします。こういうクラスライブラリ開発は、すごく楽しいです! --ちなみにこのACleanと(続編に書いた)自作のメモリアロケータを合わせても2~3KBくらいなので、C++の15KBと比べたら、私としてはかなり満足です。 --なんかそれまでのC言語とはちょっとちがう使い心地です。 -続編 → [[a22_memman07]]