くだらない動機(C++がうらやましいけど、C++は使いたくない)でささやかな規模のものを作っただけだったのに、意外に役立つ面白いものができた。
これをもっとすっきり書ける自作言語ができたら、相当に面白いことになる気がしている。
C言語でメモリ管理を考えた話サイボウズ・ラボ川合秀実
View Slide
出発点(最初のきっかけ)• C++は便利な機能がたくさんあるけど、C++でクラスをある程度使うと(継承とか仮想関数とかを使わなくても)、実行ファイルが10~20KB増えてしまいます。• C言語で同じ処理をする場合と比較しています。• リンクされるライブラリが増えるから?• 10~20KBなんて大したことないといえばそうなのですが、私はたいてい20~100KBくらいの開発をするので、C++を使うだけで1~5割程度大きくなってしまうことになり、それが私には不満だったので、「C言語だけでどこまでやれるか」を頑張ってみることにしました。
でもC++がうらやましい• C++では、オブジェクトを宣言しているスコープ(コードブロック)を抜けると、自動でデストラクタを呼んでくれる機能があります。• 当たり前ですが、C言語にはそんな機能はありません。明示的にデストラクタ関数を呼ぶ必要があります。• 私はC++のこれがうらやましくてたまりません。デストラクタを呼び忘れないように気を付けるのは生産的じゃないです。{….ClassAbc abc(1, 2, 3);….…} // スコープを抜けると自動でabcのデストラクタを呼びだしてくれる
Cleanクラス• 似たようなことをどうしてもやりたくて、以下のようなものを作りました。• Cleanクラス• 関数のポインタと引数を複数登録できる構造体• Clean_out()関数で、それらの関数を登録とは逆順に全部呼び出す関数ポインタ 引数1 引数2 引数3関数ポインタ 引数1 引数2 引数3関数ポインタ 引数1 引数2 引数3関数ポインタ 引数1 引数2 引数3登録方向 呼出方向
Cleanクラス• コンストラクタ関数は、引数にCleanオブジェクトへのポインタを受け付けるようにします。そして初期化が済んだら、Cleanオブジェクトに自身のデストラクタ関数を登録します。void ClassAbc_init(…, Clean *clean) // ClassAbcのコンストラクタ{……Clean_set(clean, &ClassAbc_deinit, …); // ここでデストラクタを登録}void ClassAbc_deinit(…) // ClassAbcのデストラクタ{…}
Cleanクラス• メモリを確保する関数(aMalloc)もCleanオブジェクトへのポインタを受け付けるようにして、malloc後に、freeのための関数呼び出しをCleanオブジェクトに登録します。• これで、どのデストラクタを呼ばなければいけないかとかに悩むことなく、スコープに入ったらCleanオブジェクトを初期化して、スコープを抜けるときに機械的にClean_out()を1回呼ぶだけでOKになりました。{Begin(clean); // Cleanオブジェクトを宣言して初期化するマクロchar *p = aMalloc(1234, &clean); // malloc(1234)相当…Clean_out(&clean); // pはこのタイミングでfreeされる}
本当に悩みは減ったのか?• [step1]• まず、mainの冒頭でCleanオブジェクトを初期化。• あとはClean *が必要になったときに、全部このCleanオブジェクトのポインタを渡す。• mainの最後にClean_out()を記述。• つまりCleanオブジェクトは全体で1つだけ。とりあえずこれで動く!• これで問題がなければ、これでおしまい。• [step2]• メモリを使いすぎてよくないところを見つけたら、それを囲める範囲で、新しいCleanオブジェクトを初期化。そして、この範囲内のオブジェクトのうち、外のスコープに出さなければいけないものだけをcloneして、範囲外でも使えるようにする。• 厳密に考えすぎず、「ここに来たらもう絶対に触らない」と思えるところでClean_outすればいいだけ。・・・こう思ったら楽でした。
本当に悩みは減ったのか?Begin(clean);オブジェクトをたくさん作って、最後にたくさん捨てる範囲Clean_out(&clean);演算の結果など、外に出さなければいけないオブジェクト外に出さなきゃいけないものだけを、外のcleanオブジェクトのポインタを指定して、クローンする結果などのオブジェクト外側のスコープ
オブジェクトがなくなったらClean_out• [Q] 単純なスコープではなく、あるオブジェクトがなくなったタイミングでClean_outしたい。どうしたらいいか?• [A] そのオブジェクトのメンバーにCleanオブジェクトを持たせて、そのオブジェクトのデストラクタ内で、Clean_outすればいいと思います。
Cleanが使えないケース• オンメモリの簡易データベースのクラス• データをINSERTしたりDELETEしたり• DELETEしたものをデストラクタのClean_outで片付けようとすると、データベースのデストラクタを呼びだすまで片付かないことになる(それはきっとまずい)• だからこれはClean_out方式が向いていない• こういうときは、従来通りの方法(自分でfree)でやる。• Clean *を指定する文脈でNULLを指定すれば、デストラクタやfreeをCleanオブジェクトに登録しなくなるので、それらについては自前で管理するようにする。
そのほかの補助ツール• 「コンストラクタが呼ばれたらポインタを登録して、デストラクタが呼ばれたら、登録ポインタを削除する表」を作ったので、登録状態を確認することで、デストラクタの呼び忘れを見つけやすくなりました(デバッグ時のみ有効)。• メンバ関数では、冒頭でポインタが前述の表に登録済みかを確認するので、デストラクタ呼び出しが早すぎる場合も気づけます(デバッグ時のみ有効) 。• 「mallocしたら登録して、freeしたら削除する表」もあるので、メモリリークも見つけられます(デバッグ時のみ有効) 。• Cleanオブジェクトはスコープのネストを登録可能で、中にClean_outしていないオブジェクトが残っている場合、「エラーにする」or「先に内側をClean_outする」を選べます。通常は書き忘れが疑われるのでエラーを選びますが、longjmpのときは内側Clean_outを選びます。
ここまで作った上の感想• もうオブジェクトの寿命では悩まない!• ミスがないことにも自信が持てるようになりました。• Clean_outを使うと、実行ファイルは1~2KBほど増えます。これは私としては十分に許容範囲です!• Clean_outすら書かなくてもいいC++や、ガーベージコレクションのある言語(たとえばJava)には負けるけど、C言語の軽さも魅力なので、私はもうあまり他言語をうらやましいとは思わなくなりました。私としては、CleanクラスがあればC言語はいいバランスです!• 現状では、何をするにもCleanオブジェクトのポインタ指定が必要で、それが少しうっとうしいので、自作言語でこれをきれいに書けるようにしたいです。なんなら、ブロックを抜けるときに、自動でClean_outするコードを出力させればいいかもしれないです。
さらに応用: xsprintf()char *xsprintf(Clean *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;}char *s = “”;Begin(clean); // Cleanオブジェクトを初期化for (I = 0; I < 100; i++)s = xsprintf(&clean, “%s %d”, s, i);puts(s);Clean_out(&clean);sのサイズとかを気にせずに、xsprintfでペタペタと文字列をつないでいるだけ。気楽!(まあ多分この書き方は遅いだろうけど)こんなに雑なことをしても、clean-outで全部片づけられる。デストラクタの呼び忘れ防止だけではなく、もっと積極的に活用してみました!
Matrixクラスの例(行列クラス)• add、sub、mulはMatrix *を返しています。• C言語ではメンバ関数が使えないですが、自作のトランスコンパイラを使って、ごまかしています( -> の代わりに . が使えます)。• (add、sub、mulの引数内の&cがうっとうしい・・・)Begin(c);Matrix *A = Mat22_init(1, 2, 3, 4, &c);Matrix *B = Mat22_init(5, 7, 8, 6, &c);Matrix *C1 = A.add(B, &c).mul(A.sub(B, &c), &c); // (A+B)*(A-B)Matrix *C2 = A.mul(A, &c).sub(B.mul(B, &c), &c); // A*A-B*BC1.print("C1=");C2.print("C2=");Clean_out(&c);
C++ との比較[clean-out前提の方式 (Javaに似ている)]変数の寿命が尽きても、clean-outするまではオブジェクトは消えない。だから以下ができる。変数(ポインタ)が指しているオブジェクトはいろいろ。誰からも参照されない値があってもいい(オブジェクト1)複数の変数から参照されていてもいい(オブジェクト2)abc def ghiオブジェクト1 オブジェクト2オブジェクト3[C++方式 (参照カウントを使わない場合)]変数の寿命が尽きれば、オブジェクトは消える。変数が指しているオブジェクトは固定。「どのオブジェクトを指すか」を変えずに、オブジェクトの中身を書き換えることで、値を変更する。変数の寿命が尽きたらオブジェクトが消えちゃうので、他の変数が参照していては危ない。だから abc = ghi; ってするだけで代入できる。ムーブとかは気にしないでいい。ただし、オブジェクトは原則として書き換えない。変更した値が欲しければ、新規にオブジェクトを作って、そこを参照させる。
xsprintf2()char *xsprintf(Clean *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”が頻出なら、こんな特別処理を書くこともできる。“0”はCleanに登録しないので少し速い。メモリも節約できる。C++では、「ヒープメモリ上のポインタを返す」か「文字リテラルを返す」か、どちらなのかわからない関数は基本的によろしくない。(freeするべきかどうか判断に困るから。)でもclean-out方式なら、登録しないだけなので、問題なくできる。
まとめ• くだらない動機(C++がうらやましいけど、C++は使いたくない)でささやかな規模のものを作っただけだったのに、意外に役立つ面白いものができたと思います。• 簡易データベースの例のように、この枠組みですべてカバーできるというわけではないけど、だからといってClean_outがダメだということはないと思います。得意・不得意があるのは気にしなくていいと思います。わずかな短所のために、多くの長所をあきらめるのはもったいないです。• これをもっとすっきり書ける自作言語ができたら、相当に面白いことになる気がします!
感想• きっとこういう「くだらない動機でちょっとしたものを作ったら、意外に面白いことになる」ことって、もっと他にも埋もれている気がします。• 立派なものじゃなくても、簡単そうなものでも、とにかく思いついたら作ってみるのがいいのかな、なんて思いました!