MISRA-Cにおける「関数の末尾以外の return 禁止」の真意

MISRA C という失敗 (#2295472) | コーディング標準は役に立つのか | スラド
早期returnとMISRA-Cルール14.7は背反か否か. - Togetter

MISRA-Cはそもそも、

  1. どのルールを守り、どのルールから逸脱するのか検討し、決定する。
  2. 上記の決定内容とその理由(順守するルール、逸脱するルール、逸脱の理由と範囲など)についてドキュメント化する。

――という工程を経る前提で作られているルール集なので、「使い物にならない制約」と感じるのなら逸脱して構わない(ただしプロジェクト内で議論したうえで、必要な部分はしっかりドキュメント化してね)のだけど、それは置いておいて。

付け加えるのなら、自分なら例えば「使用条件をドキュメント化した上で限定的にgotoを許可する*1」とかやるだろうけど、それも置いておいて。

MISRA-Cにて「関数の末尾以外の return 禁止」というルールはどこからきていて、どういう意味を持っているのだろうか?

MISRA-C:1998 では、関数についてのルールである82「(推奨)関数は1つの出口しか持ってはならない」に起因する。ルールの目的は可読性と保守性の向上のようだ。制御の流れを強制的に中断することで読み手の混乱を招く可能性や、制御の流れが分かれることで修正箇所が複数に分かれる可能性(それによる修正漏れの危険性*2)を警戒している。

ただしこのルールは推奨ルールだ。推奨ルールは「“通常従う方がよい”要求」で、ルールを守れなくても逸脱の手続きは不要だ。

MISRA-C:2004 においては、制御フローについてのルールである14.7「(必要)関数では、関数の最後に唯一の出口がなくてはならない」が該当する。MISRA-C:1998 とは異なり、必要ルールだ。逸脱する場合はプロジェクト内で議論した上で、何らかのドキュメント化が必須となる。

この変化は何を意味するのか? MISRA-Cは教条的に「関数の出口は1つにする」というスタイルに固執するようになったのか? おそらく違う。

真相は、多分「逸脱の手続きを踏ませる為に必要ルールに変更した」というところだと思う。

「関数の出口を1つに云々」というのは、構造化定理とダイクストラ流の構造化プログラミングが入り交ざった話だ。

構造化定理にて、プログラマ向けに言い換えたところの「1つの入り口と1つの出口を持つプログラムは、順次・選択・反復の3つの論理構造の組み合わせで表現できる」ということが証明された。

ダイクストラの構造化プログラミングでは、以下のような話が出てきた。

  1. 小さなプログラムでは、そのプログラムが正しいことを数学的手法や論理的手法で検証できるが、大きなプログラムではその方法が使えない。
  2. (2012/12/26追記)ソフトウェア・テストはバグの存在を証明するが、バグの不在を証明することはできない。
  3. (1)〜(2) より、プログラムが正しいことを検証するために、人間がソースコードを読んで理解する必要がある。
  4. (2012/12/26追記)(3) を達成するためには、最初から正しいプログラムを記述しなくてはならない。つまり、読むことで正しさを検証できるようなプログラムの記述・設計が必要だ。
  5. (4) を達成するために、まずソースコード(プログラムの静的構造)と実行時のフロー(動的構造)を一致させる必要がある。でなければ、ソースコードを読む作業だけでなく、実行時の実際のフローを追いかける作業が発生し、結果として人間の頭では到底プログラムが正しいか検証できなくなる。
  6. (5) を達成するために、経験的にgoto文は使うべきではない。*3
  7. gotoの代替として、構造化定理で出てきた3つの論理構造(順次・選択・反復)を入れ子状に組み合わせて使用するべきである。
  8. (7) の流儀でプログラムを組み立てると、論理構造が多層に積み重なった状態になる。この時、ある階層以下を切り出してみると、切り出した部分は「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」である。

ダイクストラの構造化プログラミングにおけるサブルーティンの使用に関しては、まだ理解しきれていない部分があるのだけど、上記の (8) あたりより、サブルーティン自体も「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」だったりする。

ここで注意すべきなのは、構造化プログラミングの文脈では、「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」というスタイルはプログラム実行時のフローをソースコードから追いやすくするために用いられている、という点だ。

で、関数の出口を1つにするというスタイルも、結局は「フローを追いやすくする」という目的を持っている。MISRA-Cのルールはこのあたりが結構厳し目で、break・continue・gotoの使用に関しても慎重な立場を取っている。breakやcontinueは用途限定のgotoなので、gotoと並んでソースコード(静的構造)と実行時のフロー(動的構造)の乖離を起こしやすい構文なのだ。

ただ、このスタイルはいい線いっているけど銀の弾丸ではない訳で、厳格に適用してみるとフラグが必要になってゴチャゴチャする等の問題が発生したりする。これを避けようとすると、特定の場面においてbreakやcontinueや途中returnを使わざるをえなくなる。C言語では特定の場面でgotoも使うことにもなる。

現在のC言語系のプログラミング言語にbreakやcontinueや関数内限定のgotoが残っていたり、途中でreturn可能な言語仕様だったりする理由は、「乱用したらNGだけど、でも代替のよい案がない」からだ。つまり微妙に必要悪的なポジションの機能なのだ。

更にいえば、構造化プログラミングの段階では静的構造と動的構造の乖離を問題視しているにすぎない――静的構造自体が複雑怪奇であるケースを考慮していない。どうもこのあたり、ダイクストラの構造化プログラミングの段階では、適切な階層化とサブルーティンによる階層の切り取りと閉じ込めによって回避できる、と考えていた節があるように感じる(気のせいかもしれないけど)。(2012/12/26追記:もっとも「読むことで正しさを検証できるようなプログラム」を求めようという姿勢からすると、構造化プログラミングでは明示されていないものの、静的構造の複雑化は許されないだろう)

関数内での途中returnは、大抵がネストを浅くする――つまり静的構造自体が複雑化することを避ける目的で使用されることが多いはずだ。構造化プログラミングでは考慮されていない部分なのだ。

こうなってくると、基本的には「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」というスタイルをとりつつも必要に応じてルールを破る、というスタイルに落ち着く。

しかし悩ましいことに、プログラマが10人いればルール破りの範囲も10通りになるのだ。

こういう現実の泥臭い話があるので、MISRA-Cでは基本的に「静的構造と動的構造の一致」というスタンスを取りつつもルール破りを許しているし、「ルール逸脱の手続き」という形でドキュメント化を促すことでルール破りの範囲をプロジェクト・組織内で統一させようとしている。

MISRA-C:2004 14.7「(必要)関数では、関数の最後に唯一の出口がなくてはならない」は「このスタイルに固執せよ」ではなく「ルール破りはOKなので、ちゃんと議論した上でドキュメント化してね」という意味なのだ。

MISRA-Cの部分の参考文献

組込み開発者におくるMISRA‐C―組込みプログラミングの高信頼化ガイド

組込み開発者におくるMISRA‐C―組込みプログラミングの高信頼化ガイド

組込み開発者におくるMISRA‐C:2004―C言語利用の高信頼化ガイド

組込み開発者におくるMISRA‐C:2004―C言語利用の高信頼化ガイド

構造化プログラミングの部分の参考文献

構造化プログラミング (サイエンスライブラリ情報電算機 32)

構造化プログラミング (サイエンスライブラリ情報電算機 32)

ソフトウェア・グラフィティ

ソフトウェア・グラフィティ

*1:エラー処理や多重ループの脱出など。他のモダンな言語ならラベル付きbreakとか色々と便利機能があるが、C言語ではgotoで代用することになる。実は、個人的にはエラーによるリトライのような「本質的にループ処理ではないループ」で「goto RETRY;」的にgotoを使いたいのだけど、このあたりになると組織内での意思統一が難しくなってくる。

*2:例えば関数の戻り値の型や意味を変更した場合、returnが複数あるのなら、その全てを修正する必要がある。

*3:ちなみに、昔のgotoはC言語のgotoと異なり、他のサブルーティンの中にジャンプすることができた。