解放のお悩みをTemplateパターンで解決!
はじめに
こんにちは、42TokyoでC言語を用いた実践的なCSを学んでいる うらっしゅです!
C言語でのプログラミングにおいて、エンジニアが頻繁に直面する問題のひとつが、動的に確保したリソースの解放漏れではないでしょうか?
特に malloc
を使った場面では、メモリ確保の成功可否を判定し、失敗時はそれまでの割り当てを手動で解放しなければなりません。
このようなコードは、リソースの獲得・解放処理が本来の目的であるロジックを囲い込んでしまい、コードの可読性や保守性を大きく損なう傾向があります。
複数のリソースを扱う処理では、途中でエラーが発生したときに、どのように安全に後始末(clean-up)を行うか、しばしば煩雑な制御構造が必要になります。
そこで本記事では、この問題を解決するアプローチとして、オブジェクト指向設計で知られる テンプレートメソッドパターン を、C言語流に応用する方法をご紹介します。
対象読者
- C言語で
malloc
やfopen
を使った処理に慣れてきた初中級者 - メモリリークやリソース解放漏れに課題感を持っている方
- 既存コードが
goto
やif
のネストで煩雑になっていることに悩んでいる方 - 設計・実装の観点から「きれいな後片付け」の方法に関心がある方
- オブジェクト指向の設計パターンを関数型/手続き型言語で応用してみたい方
このコードはリークしない...?
まずは、以下のコードを見てみてください。
fopen
関数を使った、典型的な「ファイルの内容を読み取って加工する」処理の一部です。
ファイルの内容を出力する関数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int read_and_process_file(const char *filename) {
FILE *fd = fopen(filename, "r");
if (fd == NULL) {
fprintf(stderr, "ファイルを開けません: %s\n", filename);
return -1;
}
char *line_buf = malloc(1024);
if (!line_buf) {
fprintf(stderr, "メモリ確保に失敗しました。\n");
fclose(fd);
return -1;
}
if (fgets(line_buf, 1024, fd) == NULL) {
fprintf(stderr, "ファイルの読み込みに失敗しました。\n");
return -1;
}
// 読み込んだ`line_buf`を加工する処理
// ~~~~~~複数行~~~~~~~~
fclose(fd);
free(line_buf);
return 0;
}
お分かりでしょうか...?
そう、fgets
で読み込みに失敗した場合、fclose(fd)
が呼ばれないまま return
してしまっています。
ファイルディスクリプタ(fd)が開きっぱなしになってしまい、これではリーク確定です。
仕方ない、fclose を足すかぁ
if (fgets(line_buf, 1024, fd) == NULL) {
fprintf(stderr, "ファイルの読み込みに失敗しました。\n");
free(line_buf);
fclose(fd);
return -1;
}
確かに、これでファイルディスクリプタの解放はできて、リークは防げるようになりました。
でも……ちょっと気持ち悪さ、ありませんか?
「メモリも手動で確保してるし、エラー箇所が増えるたびに free
と fclose
を追加するの面倒だなぁ…」
「解放コードがバラバラに散ってて、うっかり漏れそう」
そんな感覚、経験したことある方も多いのではないでしょうか?
そして、ここではまだたった2つのリソースしか扱っていません。
もっと複雑な処理になったとき、果たしてミスなく解放できるでしょうか?
Tamplateパターンを適用すると
では、Templateパターンを適用した場合をみてみましょう
Templateパターン使った場合
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// テンプレート関数(共通のリソース管理を行う)
int read_file(const char *p_file_name, int (*processor)(FILE *fp)) {
FILE *fp = fopen(p_file_name, "r");
if (fp == NULL) {
fprintf(stderr, "ファイルを開けません: %s\n", p_file_name);
return -1;
}
// 具体的な処理を委譲
int return_value = processor(fp);
fclose(fp); // 解放はテンプレート関数側で一括
return return_value;
}
// 呼び出し側で定義する処理(具体的な加工処理)
int process_file(FILE *fp) {
char *line_buf = malloc(1024);
if (!line_buf) {
fprintf(stderr, "メモリ確保に失敗しました。\n");
return -1;
}
if (fgets(line_buf, 1024, fp) == NULL) {
fprintf(stderr, "ファイルの読み込みに失敗しました。\n");
free(line_buf);
return -1;
}
// 読み込んだ`line_buf`を加工する処理
// ~~~~~~複数行~~~~~~~~
// 解放
free(line_buf);
return 0;
}
int main(void) {
// ここで`read_file`の関数ポインタに`process_file`で注入を行っている。
if (read_file("example.txt", process_file) != 0) {
fprintf(stderr, "ファイル処理に失敗しました。\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
🧠 なぜこれがTemplate Methodパターン?
-
read_file
が「テンプレート関数」にあたり、共通化された処理の枠組みfopen → 処理 → fcloseを提供します。 -
process_file
が「具象メソッド」として、可変的な処理の内容を関数ポインタで差し込んでいる構成です。 -
main
側からは、read_file
を使うだけで、安全にリソースを開放できる設計になっています。
✅ メリット
- fcloseの書き忘れなし
- 処理ごとに
fopen/fclose
を書かなくて済むため、可読性が上がる - リソース数が増えても 管理の責任を分離できる
- 例外のないC言語でも、構造化されたエラーハンドリングを模倣できる
まとめ:Templateパターンでリソース解放の漏れを防ぐ
C言語では、リソースの確保と解放を手動で管理しなければならず、メモリリークやファイルディスクリプタの閉じ忘れが起きやすい環境です。
特に複数のリソースを扱う処理では、エラー発生時のクリーンアップコードが煩雑になりがちです。
この問題を解決する1つのアプローチとして、Template Methodパターンの考え方を応用することで、共通の処理(リソースの獲得と解放)をテンプレート関数側に集約し、可変な処理(実際の業務ロジック)を関数ポインタで外部から注入する構成にできます。
この設計によって、
- fclose や free の書き忘れを防ぎ
- 本来の目的に集中できる可読性の高いコードが書け
- リソース管理の責務を関数レベルで分離できる
という明快なメリットが得られます。
Templateパターンでリソース解放の漏れ問題を全て防げるわけではありませんが、「fopen + fclose」「malloc + free」のような定型的な取得・解放セットを安全に処理したい場面では非常に有効なので、試してみてください。
適材適所でこの設計パターンを使えると、C言語でも驚くほど美しく、安全なコードが書けるようになるでしょう。
Discussion