エラー通知方法の古今東西

ちょっと前に,エラー通知の方法について一部界隈で盛り上がっていました.それを見てるうちに,そう言えばエラー通知方法ってあまり意識した事ないなと感じたので,ここで一度纏めてみます.尚,以下は C 〜 C++ を対象として記述しているので,他言語だとまた状況が異なる部分もあるかもしれません.

エラー通知方法を考える際に問題になる事は,以下の 3 つに大別されるかと思います.

  • 正常値/エラー値に何を割り当てるか
  • エラー通知と関数適用結果をどのように共存させるか
  • (エラーが発生した事実の通知だけではなく)エラー内容をどのように通知するか

以下,これらについてそれぞれ纏めてみます.例外との兼ね合いもあるのですが,ちょっと長くなりすぎたので今回は割愛します.

戻り値を用いたエラー通知

整数値によるエラー通知

恐らく,C の最初期の頃から(今でもずっと)行われている方法は,返す整数値の値でエラーかどうかを通知すると言うものでしょうか.整数値に関しては,「ゼロ」の意味をどうするかでしばしばユーザを混乱させてきたと言う歴史があります.

例えば,C 標準ライブラリの isalnum()/isalpha()/... などの文字判定関数は,条件に合致すればゼロ以外の値,合致しなければ(≒エラー)ゼロを返します.一方で,同じ C 標準ライブラリでもファイル操作関数である remove()/rename() は正常終了の場合はゼロ,異常終了の場合はゼロ以外の値を返します.このように,単一のライブラリに限って見ても,異なるゼロの値の扱い方が混在しているために,ユーザが誤った条件式を書いてしまう,関数を使うたびにリファレンスで調べなければならないなどの面倒をユーザに強いる,などの問題が発生しました.

この問題を受けてか(後述するエラー内容の通知との兼ね合いもあるでしょうが),正常時/エラー時,どちらにおいてもゼロは返さないと言う規則を取るライブラリを見かけるようになりました.よく見る規則としては,正常時には正の整数,エラー時には負の整数と言うものがあります.

しかし,この方法にも問題が存在しました.この規則を適用するライブラリの多くは上記のように正数/負数と言う括りではなく,ある特定の値(正常値が 1,エラー値が -1 と言う場合が多いように感じる)を返すと言う説明になっているため,ユーザがそれぞれの数値を覚えておかなければならないと言う問題が発生しました.プログラミングにおいては,具体的な整数値はマジックナンバーとして嫌われるので,しばらくすると 1 を OK,-1 を ERROR のようにマクロで定義する事が主流となり具体的な数値を覚える必要はなくなりました.しかし,その代わりに星の数ほどの XXX_OK と XXX_ERROR を生みだす事になり,異なるライブラリ間で整合性を取るときなど,時にはマジックナンバーよりもやっかいな存在になる事もありました.

ポインタ値によるエラー通知

整数値によるエラー通知と同じ位(特に C で)よく使われる方法にポインタ値によるエラー通知があります.ポインタ値は,何も指してない事を表す NULL と言う特別な値が定義されているため,この値をエラー値として利用すると言う方法が多くのライブラリで採用されてきました.

ポインタ値によるエラー通知は NULL 値の性質(if (!p) {...} と言う形でエラー判定ができるため直感通りに記述できる,NULL と言う名前で定義されている)のおかげもあり,エラー通知と言う観点から言えば,整数値によるエラー通知よりもユーザを混乱させる事が少ないと言う好結果を生みました.

bool によるエラー通知

C++ では bool 型が言語機能として組み込まれたため,前述した「ゼロの扱いの混在」や「星の数ほどの独自の OK/ERROR のマクロが存在する」と言う問題は収束していきました.

エラー通知と関数適用結果を戻り値で共存させる方法

何らかの関数を記述する際に発生する要求の一つに,「エラーかどうかを通知する為だけに戻り値を占有されたくない」と言うものがあります.この要求とどう兼ね合いを付けるか,と言うのもエラー通知方法のテーマの一つです.

共存可能なポインタ値

幸いな事に,ポインタ値によるエラー通知方法は既にこの課題をクリアしていました.正常終了の場合は,その関数が生成した結果へのポインタ,異常終了の場合は NULL を返すと言う規則を適用する事によって,戻り値のみで関数の適用結果とエラー通知を同時に実現できる事になります.

当初どの程度意図されていたかは分かりませんが,エラー通知と言う機能だけに限ってみれば,ポインタを返すと言うインターフェースはかなり優れていたのだなぁと感じます.

取り得る範囲外の値をエラー値とする方法

整数値を用いてエラー通知を行う場合に戻り値において関数適用結果と共存させる方法として,「その関数が戻り値として取り得る範囲外の値をエラー値とする」と言う規則を適用する事があります.

例えば,C 標準ライブラリの getchar() 関数は標準入力から一文字読み込んでその値を返すものですが,戻り値の型が char ではなく int になっています.これは,char 型の取り得る値の範囲外で EOF と言う特別な値を定義し,エラー時(読み込む文字が存在しない)にはその値を返すと言う規則を採用しているためです.また,double 型の NaN や Inf のように型自体にいくつかのエラー値をあらかじめ定義しておき,正常時においてはその値は取らないようになっているものもあります.このように,本来取り得る値よりも範囲を広げる事によってエラー通知と共存すると言う方法はいくつかの条件の下では効果的に使用されています.

しかし,この方法は「取り得る範囲外の値が存在しない」場合には採用する事ができません.また,戻り値の型が構造体であった場合もこの方法を適用する事は難しくなります.このように,ポインタを戻り値に返す場合に比べて「適用できない状況」が多いと言う問題が存在します.

std::pair を戻り値とする方法

上記に代わる方法として,C++ においては std::pair を返す方法が用いられる事があります.これは一見良い方法であるように見えるのですが,思ったほど利用はされてないと言う感想です.恐らく,std::pair と言う型宣言をユーザに強いるのがデメリットになっているのではないかと思いますが,自信がないので意見募集中です(参考:std::pair<bool, xxx_type> が流行らなかった理由 - Togetter).

Boost.Optional を戻り値とする方法

前述した問題を解決する手段として,Boost では Boost.Optional が提案されています.Boost.Optional では,内部でエラー値を定義しておき,正常時には関数適用結果を,エラー時にはエラー値を(ラップした Boost.Optional クラス)返す事でどのような型であっても getchar() 関数などと同じ規則を適用できるようになっています.

エラー通知と関数適用結果を戻り値で共存させない方法

別の方法として,最初からエラー通知と関数適用結果を両方とも戻り値で返そうとは考えないと言うものがあります.すなわち,エラー通知か関数適用結果のどちらかを戻り値以外の方法で返そうと言うものです.

エラー通知のみ戻り値で行う方法

関数を定義する場合においては,エラー通知の方を戻り値で行い,関数での適用結果は引数を通じて行うと言う形を取る事の方が多いのではないかと思います.

int f(xxx_type* dest);
bool f(xxx_type& dest); // C++ のみ

ユーザ側であらかじめ適用結果を格納するための領域を用意してもらい,関数の引数に指定してもらうと言うものです.C++ では参照型が組み込まれた事によって,ユーザに f(&dest); のように & の記述を強いる事がなくなったので,やり易くなったのではないかと思います.

関数の適用結果を戻り値で行う方法

正確に言うと「エラー通知を戻り値以外の方法で通知する」でしょうか.例えば,C++ の IOstream クラスはいくつかのメンバ関数において,その関数が異常終了した場合に std::ios::failbit や std::ios::badbit を設定する事になっています.そのため,ユーザは該当のメンバ関数を実行した直後に fail() や bad() を通じてそのメンバ関数が正常に終了したかどうかを知る事ができます.

エラー内容の通知方法

ここまでは「関数が異常終了した事」を伝える方法についての話ですが,「異常終了したと言う事実だけではなくその内容も知りたい」と言う要求もしばしば発生します.

エラー内容に対応したエラー値を返す方法

戻り値として整数値を返す方法については,この要求は比較的容易に応えられます.あるエラー内容を -1,別のエラー内容を -2,と言うようにエラー内容毎に異なるエラー値を定義しておく事によって,ユーザはその戻り値から「エラーが発生した事」だけではなくその内容も知る事ができるようになります.

一方で,戻り値としてポインタを返す方法については,エラー値は NULL の一つしか使えないため,他の方法を必要とします.Boost.Optional もポインタと同様の問題を抱えており,「エラー内容を通知する」と言う機能まで含めて考えると,これらの方法も万能ではないようです(参考:boost::optionalだけじゃなくboost::eitherがほしい - Faith and Brave - C++で遊ぼう).

エラー内容は別の方法で返す方法

前述したように,ポインタや Boost.Optional では「エラー内容」までは含める事ができないので,エラー内容を通知するためには別の方法を模索する必要があります.これに対する一つの解として,C 標準ライブラリ(+α?)では,errno と言うグローバル変数を用いてエラー内容を通知すると言う方法が採用されています.ある関数が異常終了した場合,その関数の戻り値では(NULL などの値を用いて)エラーが発生した事のみを通知し,エラー内容については errno やその補助関数を通じて通知すると言う方法が取られています.

また,多くのライブラリで見られる別の解決方法として,エラー内容を格納するためのバッファを(関数引数を通じて)ユーザに用意してもらうと言う方法を取る事もしばしば見られます.これらの解決方法に関してはどちらも一長一短であり,どちらを採用するのか(もしくは,これらとは異なる方法を模索するのか)は,各ライブラリの設計者の方針に依ってきます.

以上,何か想像以上に長くなってしまいましたがざっと纏めてみました.エラー処理に関しては,一時期は例外処理で統一されるのかなぁと思っていたのですが,様々な理由で残念ながら(?)そううまくはいかないようです.