はじめに
42で1番沼ったminishellから学んだエラー処理と設計についてのお話です。
42で課題を進めていると、最初はこう考えがち(こんな感じでもなんとかなりがち)だと思います。
- エラーが起きうることは分かっている
- 必要そうなチェックは入れている
- 足りなければ後から足していけばいい
私はまさにこんな状態でした。
しかし minishell で一度本気で詰んだことで、この考え方が「設計としては不十分だったと痛感しました。
この記事では、
- エラー処理は「実装」ではなく「設計」だったこと
- return / exit / 構造体に状態を持たせる判断
- close(fd) の責任は誰が持つのか
- ft_error_and_exit に行き着くまでの試行錯誤
を、minishell の失敗エピソードを交えて書きます。
エラー処理は「ある程度把握してから足していけばいい」と思っていた
正直に言うと、
「エラー処理はあとで考えればいい」とまで雑に考えていたわけではありません。
- open / malloc / fork が失敗しうることは分かっている
- return 値を見る必要があることも分かっている
- 最低限のエラー処理は入れているつもりだった
なので当時の自分の感覚は、どちらかというとこうでした。
まず全体の流れを作る
想定できるエラーは入れておく
足りなかったら、あとから補えばいい
libft や fdf の規模感では、
このやり方でも「それなりに」成立してしまいます。
しかし minishell でこの姿勢のまま進めた結果、
エラー処理が「設計」ではなく「場当たり的な追加」になっていたことに気づきました。
「把握しているつもり」と「設計している」の違い
「把握しているつもり」と「設計している」の違いは、
何が失敗したときに、どこまで・誰が責務を持つのかを言語化できているかだと思います。
エラーが起きうることを理解していても、
「この関数はどこまで後始末をするのか」「ここで失敗したら上位に何を伝えるのか」「ここから先は絶対に戻らないのか」といった点を、
なんとなくの感覚で決めているうちは、それは設計しているとは言えません。
minishell で詰んだ原因も、エラーそのものではなく、
責務の境界を曖昧にしたまま実装を進めてしまったことでした。
エラー処理を「把握している」状態から一段階進めて、
「どのレイヤがどこまで責任を持つのか」を明示できて初めて、設計として機能し始めるのだと感じました。
minishellで本当に詰んだ瞬間
minishell では、
- パース
- 環境変数展開
- リダイレクト
- パイプ
- fork / exec
- ファイルディスクリプタ管理
など、失敗しうるポイントが一気に増えます。
ある時、私はこんな状態に陥りました。
- どこかで open() が失敗
- fd が中途半端に残る
- 次のコマンドで挙動がおかしい
- 最終的に segfault、もしくは fd が枯渇
デバッグしても原因がなかなか分からない。
理由は単純でした。
誰が fd を close する責任を持っているのか、設計していなかった
close(fd) は誰の責任か?
当時の私は、
- open した関数で close すればいい
- エラーが出たらその場で close すればいい
と考えていました。
しかし実際には、
- open した関数
- その fd を使う関数
- エラーで途中 return した関数
が入り乱れ、
「誰が後始末をするのか」が曖昧になっていました。
結果として、
- 二重 close
- close し忘れ
- エラー時だけリーク
が頻発します。
ここで初めて気づきました。
close(fd) の位置は「実装の都合」ではなく「設計」で決めるべきだった
エラー処理は「制御フローの設計」
minishellで詰んだあと、私は考え方を変えました。
- このエラーは return で上位に伝えるべきか
- ここで exit すべきなのか
- 親プロセスか子プロセスか
- エラー状態を構造体に持たせるべきか
これらは関数を書き始める前に決めておくべきことでした。
ft_error_and_exit に行き着くまで
最終的に私は、エラー処理を次のように整理しました。
if (close(fd) == -1)
ft_error_and_exit("ERROR: Failed to close file.\n", NULL);
重要なのは関数名ではありません。
- この関数を呼んだら、もう戻らない
- ここで全て後始末をする
という 責務を明確にした設計です。
- どこで exit するのか
- どこまで戻る可能性があるのか
- どこでリソースを完全に解放するのか
を明示できるようになりました。
「誰が後始末をするか」を決めていなかった
minishell での最大の失敗はこれでした。
エラーが起きたとき、
誰が責任を持って後始末をするかを決めていなかった
エラー処理を後付けで足していくと、
- if 文が増える
- free が散らばる
- 制御フローが追えなくなる
結果、コードは簡単に壊れます。
まとめ:エラー処理は設計そのもの
42Tokyoで学んだ一番大きな気づきの一つはこれです。
- エラー処理は「実装」ではない
- エラー処理は「設計」そのもの
- 後付けはほぼ不可能
特に minishell のような課題では、
エラー処理をどう扱うか決めていない設計は、必ず破綻する
もし今、
- エラー処理をとりあえず return で済ませている
- close / free の責任が曖昧
- exit の位置が場当たり的
であれば、一度立ち止まって、
「誰が後始末をする設計なのか?」
を考えてみてほしいです。
それだけで、コードの安定度は確実に変わります。
おまけ
minishellのレビューでは、普段使うことあるのか?!というようなコマンドで破壊されます🥺
そんな時、設計ができてるチームはいつもレビュー中に原因を特定し、綺麗なまま修正できているなと感じます。
レビューと修正を繰り返して煩雑なコードができあがらないように、初めから設計を丁寧に行いたい所存です。
42生としてクリスマスを迎えるのは今年で3回目ですが、はじめてアドベントカレンダーに参加できて嬉しかったです。ありがとうございました🎄