C言語の標準関数を自作する:番外編 「安全なコード」とは?即時処刑(Fail-Fast)と絶対生存(Fault-Tolerant)
目次
はじめに
文字列処理系の主要な関数について、一通りの自作関数を作り終えることができましたね。
ポインタをバリバリ操作して、自前で文字を切り出したり、検索したりできるようになって、かなり「C言語を支配している感」が出てきたのではないでしょうか。
……ところで。
これまでの記事で、本家C標準ライブラリ(string.h)の仕様を確認していて、こんなことを思ったことはありませんか?
「なんか標準関数って、未定義動作多くない?」
「
strlen(NULL)で未定義動作になるとか不親切すぎない?」
「内部で
if (s == NULL) return 0;くらい書いておいてくれればいいのに!」
自分でコードを書くなら、絶対にNULLチェックくらい入れて、
「絶対に落ちない安全な関数」
にしたくなりますよね。
落ちない方が「安全」に決まっています。
……本当に?
今回は、C言語というよりソフトウェア開発全体に関わる、
「そもそも、安全って何?」
という話です。
「落ちない」ことが安全なのか?
「異常が起きても、泥を被ってでも絶対にプロセスを死なせずに動き続ける」
一見すると、これこそが理想の安全設計に見えます。
しかし、プログラミングの世界には、これとは逆方向の考え方もあります。
① 即時処刑(Fail-Fast)
思想:
「おかしいと思ったら、その場ですぐに止めろ!」
壊れたデータや不正な状態を抱えたまま処理を続けると、もっと後になって原因不明の問題を起こすかもしれません。
それなら、
異常発生
↓
即座に停止
↓
ログ・コアダンプなどを確認
↓
原因を特定
とした方が安全な場合があります。
② 絶対生存(Fault-Tolerant)
一方で、
「一部分の異常で、システム全体を止めるな!」
という考え方もあります。
例えば、
異常発生
↓
問題のある処理を中断
↓
エラーを通知
↓
他の処理は継続
とすることで、システム全体への影響を抑えられる場合があります。
じゃあ、どっちが正しい?
実は、
どちらも正しい
んです。
……はい、困りました。
なぜなら、「何を安全とするか」がシステムによって違うからです。
例えば、あくまで傾向としてですが、
即時処刑寄り
- PCアプリ
- Webサーバー
- 開発・デバッグ環境
などでは、異常を早期に発見し、原因を特定することが重要になります。
そのため、
異常を検出
↓
止める
↓
原因を調べる
という設計が有効な場面があります。
絶対生存寄り
一方、
- 自動車
- 医療機器
- 航空機
- 社会インフラ
などでは、一部の異常によってシステム全体が停止することが、より重大な問題になる場合があります。
そのため、
異常を検出
↓
可能な範囲で影響を局所化
↓
安全な状態を維持
という考え方が重要になる場面があります。
もちろん、これはあくまで傾向です。
実際には、どちらの分野でもFail-FastとFault-Tolerantを組み合わせて設計することがあります。
思考ロックを解除しよう
ここで大切なのは、
「自動車だから絶対生存!」
とか、
「PCアプリだから絶対Fail-Fast!」
と覚えることではありません。
ましてや、
「落ちないコードこそ安全なコードだ!」
と決めつけることでもありません。
逆に、
「異常時に止まらないコードはダメだ!」
と決めつけるのも違います。
重要なのは、
「自分が作っているシステムでは、何を安全とするのか?」
を考えることです。
例えば、
「この異常はプロセスを止めるべきか?」
「この異常は処理をスキップして継続すべきか?」
「一部が止まっても、他の機能は動かすべきか?」
「止まるなら、どうやって周囲に異常を知らせるか?」
こういったことを考えていく必要があります。
つまり、
「安全とはこういうものだ!」と最初から思考をロックしないこと。
これが今回、一番伝えたいことです。
なぜ、このシリーズでは「絶対生存」にするのか?
さて、冒頭の疑問に戻りましょう。
「なぜ本家C言語の標準ライブラリ(string.h)は、NULLチェックすらしてくれないのか?」
例えば、
strlen(NULL);
のようなコードでは、C標準は「NULLなら0を返す」とは規定していません。
そもそも、strlen() は適切な文字列を指すポインタが渡されることを前提としています。
その前提を破った場合、動作は保証されません。
つまり、
「標準ライブラリがわざと即死させている」
というより、
「呼び出し側が守るべき条件を満たしていないので、動作が保証されない」
と考える方が正確です。
では、なぜ、このシリーズではNULLチェックを入れるのでしょうか?
それは、
標準仕様を確認したうえで、「ここをどうするのか?」を自分たちで決めたから
です。
例えば、NULLが渡された場合でも、
NULLを受け取る
↓
0を返す
とすることもできます。
あるいは、
NULLを受け取る
↓
エラーを返す
でもいいでしょう。
場合によっては、
NULLを受け取る
↓
assertで停止する
という設計も考えられます。
つまり、標準仕様で動作が保証されていない部分を見つけたら、
「じゃあ、自分たちの関数ではどうする?」
と考えることができます。
そして、このシリーズでは、
「NULLが渡されてもクラッシュさせない」
という要件を自分たちで決めました。
その結果として、
if (NULL == ap_String) {
return 0;
}
のようなNULLチェックが入っています。
これは単に、
「NULLチェックを入れた方が安全そうだから」
ではありません。
標準仕様を理解したうえで、未定義となる部分について自分たちの要件を決めた結果
なのです。
「自作する」ことが目的ではない
このシリーズで標準関数を自作しているのも、単に、
「
strlen()を自分で書けるようになった!」
で終わらせることが目的ではありません。
標準仕様を確認
↓
仕様で保証されない部分を発見
↓
「ここはどうする?」
↓
自分で考える
↓
要件として決める
↓
設計する
↓
実装する
↓
テストする
この流れそのものを体験することに意味があります。
例えば、
「NULLなら0を返した方が使いやすそう」
と思ったなら、それを要件にする。
「でも、エラーと正常な0を区別できなくない?」
と考えたなら、別の仕様を検討する。
こうやって、
「自分ならどうする?」
を考えていくことが、単純にC言語の文法や関数の書き方を覚える以上の学びにつながります。
そして、その判断に絶対的な正解があるとは限りません。
大切なのは、
「なぜ、この仕様にしたのか?」を説明できること
なのではないでしょうか。
まとめ
今回の話をまとめると、
- Fail-Fastは、異常を早期に検出し、不正な状態を広げないための考え方.
- Fault-Tolerantは、一部の異常によるシステム全体への影響を抑えるための考え方.
- どちらか一方が常に正しいわけではない.
- 「落ちないこと」と「安全であること」は同じではない.
- 安全の定義は、システムの目的や要件によって変わる.
- だからこそ、「安全とはこういうものだ」と思考をロックしないことが重要.
- 標準仕様で保証されていない部分については、自分たちで要件を決めることができる.
そして 、このシリーズでは、
「NULLが来ても落ちない」
という安全思想を、私たち自身の要件として選択しました。
標準仕様を知らないから変更したのではありません。
標準仕様を知ったうえで、あえて別の仕様を選んだのです。
これもまた、プログラムを「書く」のではなく、
「設計する」
ということなのかもしれません。
さて、次にコードを書くとき。
「これは安全だからこうする」
と即断する前に、一度だけ考えてみてください。
「……そもそも、ここでいう『安全』って何だ?」
思考ロック、解除できましたか?
🔗 関連記事・関連リンク
この記事の最新アップデートや、このシリーズの関連記事は以下のリンクからご覧いただけます。
内容は基本的に同じですので、お好みのサイトでお読みください。
🟢 Zenn
-
この記事の最新版はこちら。
-
https://zenn.dev/malloc/articles/14_extra04_what_is_safety_thinking
-
「標準関数自作」Topicの関連記事はこちら。
🔵 Qiita
-
「標準関数自作」タグの関連記事はこちら。
🧡 note
-
このシリーズの記事はこちら。