Cライブラリで一貫した方法でエラーを処理するエラーに関して、「ベストプラクティス」とは何ですか。
私が考えてきた2つの方法があります。
常にエラーコードを返します。典型的な関数は次のようになります。
MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);
常にエラーポインターアプローチを提供します。
int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);
最初のアプローチを使用する場合、エラー処理チェックが関数呼び出しに直接配置される次のようなコードを書くことができます。
int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
// Error handling
}
ここでのエラー処理コードよりも良く見えます。
MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
// Error handling
}
ただし、データを返すために戻り値を使用すると、コードが読みやすくなります。2番目の例でサイズ変数に何かが書き込まれたことは明らかです。
なぜこれらのアプローチを好むのか、それともミックスするのか、何か他のものを使うべきなのか、アイデアはありますか?ライブラリのマルチスレッド使用がより苦痛になる傾向があるため、私はグローバルなエラー状態のファンではありません。
編集:これに関するC++固有のアイデアは、現時点では私にとってオプションではないので、例外を含まない限り、興味深いことです...
私はエラーを戻り値の方法として気に入っています。 APIを設計していて、ライブラリを可能な限り簡単に使用したい場合は、これらの追加について考えてください。
可能性のあるすべてのエラー状態を1つのtypedefされた列挙型に保存し、libで使用します。単にintを返すだけでなく、さらに悪いことに、intまたは異なる列挙をリターンコードと組み合わせてください。
エラーを人間が読める形式に変換する関数を提供します。シンプルにすることができます。エラー入力、const char *出力のみ。
このアイデアがマルチスレッドの使用を少し難しくしていることは知っていますが、アプリケーションプログラマーがグローバルなエラーコールバックを設定できると便利です。そうすれば、バグハントセッション中にコールバックにブレークポイントを配置できます。
それが役に立てば幸い。
私は両方のアプローチを使用しましたが、どちらもうまくいきました。どちらを使用する場合でも、私は常にこの原則を適用しようとします。
可能性のあるエラーがプログラマーエラーのみである場合、エラーコードを返さないで、関数内でアサートを使用します。
入力を検証するアサーションは、関数が何を期待するかを明確に伝えますが、あまりにも多くのエラーチェックはプログラムロジックを曖昧にする可能性があります。さまざまなエラーのケースすべてに対して何をすべきかを決定すると、設計が本当に複雑になります。プログラマーがヌルポインターを絶対に渡さないと主張できるのに、functionXがヌルポインターをどのように処理するかを理解するのはなぜですか?
Nice スライドのセット CMUのCERTには、一般的なC(およびC++)エラー処理技術の使用時期に関する推奨事項が記載されています。最高のスライドの1つは、この決定ツリーです。
私はこのフローカートについて2つのことを個人的に変更します。
まず、オブジェクトがエラーを示すために戻り値を使用する必要がある場合があることを明確にします。関数がオブジェクトからデータを抽出するだけで、オブジェクトを変更しない場合、オブジェクト自体の整合性は危険にさらされず、戻り値を使用してエラーを示す方が適切です。
第二に、C++で例外を使用するのはalwaysではありません。例外は、エラー処理に費やされるソースコードの量を減らすことができるため、優れています。ほとんどの場合、関数シグネチャに影響を与えず、コールスタックに渡すことができるデータに非常に柔軟です。一方、いくつかの理由により、例外は適切な選択ではない場合があります。
C++例外には非常に特殊なセマンティクスがあります。これらのセマンティクスが必要ない場合は、C++例外を選択することをお勧めします。例外は、スローされた直後に処理する必要があり、設計では、エラーがいくつかのレベルでコールスタックを巻き戻す必要がある場合に適しています。
例外をスローするC++関数は、例外をスローしないように後でラップすることはできません。少なくとも、とにかく例外の全費用を支払う必要があります。エラーコードを返す関数をラップしてC++例外をスローすると、より柔軟になります。 C++のnew
は、非スローバリアントを提供することでこれを実現します。
C++例外は比較的高価ですが、この欠点はほとんどの場合、例外を賢明に使用するプログラムにとって誇張されています。プログラムは、パフォーマンスが懸念されるコードパスで例外をスローするべきではありません。プログラムがエラーを報告して終了する速度は、実際には問題ではありません。
C++例外が利用できない場合があります。それらは、文字通りC++実装で利用できないか、コードガイドラインで禁止されています。
元の質問はマルチスレッドコンテキストに関するものだったので、ローカルエラーインジケーター手法( SirDarius の answer で説明されているもの)は元の回答では過小評価されていたと思います。それはスレッドセーフであり、呼び出し元がエラーをすぐに処理することを強制せず、エラーを記述する任意のデータをバンドルできます。欠点は、オブジェクトによって保持されなければならない(または何らかの方法で外部に関連付けられていると思われる)ことであり、リターンコードよりも間違いなく無視しやすいことです。
ライブラリを作成するときは常に、最初のアプローチを使用します。 typedefされた列挙をリターンコードとして使用することにはいくつかの利点があります。
関数が配列などのより複雑な出力を返し、その長さが長ければ、任意の構造を作成して返す必要はありません。
rc = func(..., int **return_array, size_t *array_length);
シンプルで標準化されたエラー処理が可能です。
if ((rc = func(...)) != API_SUCCESS) {
/* Error Handling */
}
ライブラリ関数での簡単なエラー処理が可能になります。
/* Check for valid arguments */
if (NULL == return_array || NULL == array_length)
return API_INVALID_ARGS;
Typedefされた列挙型を使用すると、デバッガーで列挙型名を表示することもできます。これにより、ヘッダーファイルを常に参照しなくてもデバッグが簡単になります。この列挙型を文字列に変換する関数があると便利です。
使用するアプローチに関係なく、最も重要な問題は一貫性を保つことです。これは、関数と引数の命名、引数の順序付け、およびエラー処理に適用されます。
setjmp を使用します。
http://en.wikipedia.org/wiki/Setjmp.h
http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html
http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
#include <setjmp.h>
#include <stdio.h>
jmp_buf x;
void f()
{
longjmp(x,5); // throw 5;
}
int main()
{
// output of this program is 5.
int i = 0;
if ( (i = setjmp(x)) == 0 )// try{
{
f();
} // } --> end of try{
else // catch(i){
{
switch( i )
{
case 1:
case 2:
default: fprintf( stdout, "error code = %d\n", i); break;
}
} // } --> end of catch(i){
return 0;
}
#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)
int
main(int argc, char** argv)
{
TRY
{
printf("In Try Statement\n");
THROW;
printf("I do not appear\n");
}
CATCH
{
printf("Got Exception!\n");
}
ETRY;
return 0;
}
初期化中にプログラムを書くとき、通常、エラー処理のためにスレッドをスピンオフし、ロックなどのエラーのために特別な構造を初期化します。次に、戻り値を介してエラーを検出すると、例外からの情報を構造体に入力し、例外処理スレッドにSIGIOを送信し、実行を続行できないかどうかを確認します。できない場合は、SIGURGを例外スレッドに送信し、プログラムを正常に停止します。
私は個人的には前者のアプローチ(エラーインジケータを返す)を好みます。
必要な場合、返される結果はエラーが発生したことを示すだけで、正確なエラーを見つけるために別の関数が使用されます。
GetSize()の例では、サイズは常にゼロまたは正でなければならないため、UNIXシステムコールと同様に、負の結果を返すとエラーを示す場合があると考えています。
私は、エラーオブジェクトをポインターとして渡して、後者のアプローチに使用したライブラリを考えることはできません。 stdio
などはすべて戻り値になります。
エラーコードを返すことは、Cでのエラー処理の通常の方法です。
しかし、最近、発信エラーポインターアプローチも実験しました。
戻り値のアプローチに比べていくつかの利点があります。
より意味のある目的で戻り値を使用できます。
そのエラーパラメータを書き出す必要がある場合は、エラーを処理するか、それを伝播する必要があります。 (fclose
の戻り値をチェックすることを決して忘れませんか?)
エラーポインターを使用する場合、関数を呼び出すときにそれを渡すことができます。いずれかの関数で設定されている場合、値は失われません。
エラー変数にデータブレークポイントを設定すると、エラーが最初に発生した場所をキャッチできます。条件付きブレークポイントを設定すると、特定のエラーもキャッチできます。
すべてのエラーを処理するかどうかのチェックを自動化するのが簡単になります。コードの規則により、エラーポインターをerr
として呼び出すことが強制される場合があり、最後の引数である必要があります。したがって、スクリプトは文字列err);
その後、その後にif (*err
。実際には、CER
(err returnをチェック)およびCEG
(err gotoをチェック)というマクロを作成しました。したがって、エラー時に戻りたい場合に常に入力する必要はなく、視覚的な混乱を減らすことができます。
ただし、コード内のすべての関数にこの発信パラメーターがあるわけではありません。この発信パラメータは、通常例外をスローする場合に使用されます。
私は過去に多くのCプログラミングを行ってきました。そして、エラーコードの戻り値を本当に評価しました。しかし、いくつかの落とし穴があります。
UNIXのアプローチは、2番目の提案に最も似ています。結果または単一の「それが間違った」値を返します。たとえば、openは成功するとファイル記述子を返し、失敗すると-1を返します。失敗すると、errno
も設定します。これは、which失敗が発生したことを示す外部グローバル整数です。
Cocoaは、その価値のあるものとして、同様のアプローチも採用しています。多くのメソッドがBOOLを返し、NSError **
パラメータ。失敗するとエラーを設定し、NOを返します。次に、エラー処理は次のようになります。
NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
// error handling
}
これは、2つのオプションの間のどこかにあります:-)。
ここに面白いと思うアプローチがありますが、ある程度の規律が必要です。
これは、ハンドル型変数がすべてのAPI関数を操作するインスタンスであると想定しています。
これは、ハンドルの背後にある構造体が必要なデータ(コード、メッセージ...)を持つ構造体として以前のエラーを保存し、ユーザーにこのエラーオブジェクトへのポインターを返す関数が提供されるという考えです。各操作はポイントされたオブジェクトを更新するため、ユーザーは関数を呼び出さなくてもそのステータスを確認できます。 errnoパターンとは異なり、エラーコードはグローバルではないため、各ハンドルが適切に使用されている限り、アプローチはスレッドセーフになります。
例:
MyHandle * h = MyApiCreateHandle();
/* first call checks for pointer nullity, since we cannot retrieve error code
on a NULL pointer */
if (h == NULL)
return 0;
/* from here h is a valid handle */
/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);
MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");
/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
fprintf(stderr, "(%d) %s\n", err->code, err->message);
MyApiDestroy(h);
return 0;
}
MyApiRecord record;
/* here the API could refuse to execute the operation if the previous one
yielded an error, and eventually close the file descriptor itself if
the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));
/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
fprintf(stderr, "(%d) %s\n", err->code, err->message);
MyApiDestroy(h);
return 0;
}
私も最近この問題を熟考し、 try-catch-finallyセマンティクスをシミュレートするC用のマクロ 純粋にローカルの戻り値を使用して記述しました。それがあなたの役に立つことを願っています。
最初のアプローチはより良いです。
私はこのQ&Aに何度も遭遇し、より包括的な回答を提供したいと考えました。これについて考える最良の方法は、how呼び出し元にエラーを返すこと、およびwhat戻ります。
関数から情報を返す3つの方法があります。
値を返すことができるのは単一のオブジェクトだけですが、任意の複合体にすることができます。エラーを返す関数の例を次に示します。
enum error hold_my_beer();
戻り値の利点の1つは、呼び出しをチェーン化して、エラー処理を軽減することです。
!hold_my_beer() &&
!hold_my_cigarette() &&
!hold_my_pants() ||
abort();
これは読みやすさだけでなく、このような関数ポインターの配列を均一な方法で処理できる場合もあります。
引数を介して複数のオブジェクトを介してより多くを返すことができますが、ベストプラクティスでは引数の総数を少なくすることをお勧めします(たとえば、<= 4)
void look_ma(enum error *e, char *what_broke);
enum error e;
look_ma(e);
if(e == FURNITURE) {
reorder(what_broke);
} else if(e == SELF) {
tell_doctor(what_broke);
}
Setjmp()を使用して、場所とint値の処理方法を定義し、longjmp()を介してその場所に制御を転送します。 Cでのsetjmpおよびlongjmpの実際の使用法 を参照してください。
エラーインジケータは、問題があることだけを示しますが、その問題の性質については何も示しません。
struct foo *f = foo_init();
if(!f) {
/// handle the absence of foo
}
これは、関数がエラー状態を伝えるための最も強力な方法ではありませんが、呼び出し側がとにかく段階的な方法でエラーに応答できない場合に最適です。
エラーコードは、問題の性質について呼び出し元に通知し、適切な応答(上記から)を許可する場合があります。戻り値、またはエラー引数の上のlook_ma()の例のようにできます。
エラーオブジェクトを使用すると、呼び出し元に任意の複雑な問題を通知できます。たとえば、エラーコードと人間が読める適切なメッセージです。また、コレクションの処理中に、複数の問題が発生したこと、またはアイテムごとにエラーが発生したことを呼び出し元に通知できます。
struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
if(reason[i] == NOT_FOUND) find(friends[i]);
}
エラー配列を事前に割り当てる代わりに、もちろん必要に応じて動的に(再)割り当てることもできます。
コールバックは、エラーを処理する最も強力な方法です。何か問題が発生したときに、どのような動作が発生するかを関数に伝えることができるためです。コールバック引数を各関数に追加できます。または、カスタマイズが次のような構造体のインスタンスごとにのみ必要な場合:
struct foo {
...
void (error_handler)(char *);
};
void default_error_handler(char *message) {
assert(f);
printf("%s", message);
}
void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
assert(f);
f->error_handler = eh;
}
struct foo *foo_init() {
struct foo *f = malloc(sizeof(struct foo));
foo_set_error_handler(f, default_error_handler);
return f;
}
struct foo *f = foo_init();
foo_something();
コールバックの興味深い利点の1つは、複数回呼び出せること、またはハッピーパスにオーバーヘッドがないエラーがない場合はまったく呼び出せないことです。
ただし、制御の反転があります。呼び出し元のコードは、コールバックが呼び出されたかどうかを知りません。そのため、インジケータを使用することも理にかなっています。
私は間違いなく最初の解決策を好む:
int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
// Error handling
}
私はそれを少し修正します:
int size;
MYAPIError rc;
rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
// Error handling
}
さらに、現在関数のスコープで許可されている場合でも、正当な戻り値とエラーが混在することはありません。関数の実装が将来どの方向に進むかはわかりません。
そして、すでにエラー処理について話しているなら、goto Error;
エラー処理コードとして。ただし、undo
関数を呼び出してエラー処理を正しく処理できる場合を除きます。
言われたことに加えて、エラーコードを返す前に、エラーが返されたときにアサートまたは類似の診断を起動します。これにより、トレースが非常に簡単になります。これを行う方法は、リリース時にまだコンパイルされているが、ソフトウェアが診断モードになっている場合にのみ起動されるカスタマイズされたアサートを使用することです。
私は個人的にno_errorをゼロとして負の整数としてエラーコードを返しますが、次のバグが発生する可能性があります
if (MyFunc())
DoSomething();
別の方法としては、失敗が常にゼロとして返され、LastError()関数を使用して実際のエラーの詳細を提供します。
エラーを返す代わりに、あなたの関数でデータを返すことを禁止する代わりにできることは、戻りタイプにwrapperを使用することです:
typedef struct {
enum {SUCCESS, ERROR} status;
union {
int errCode;
MyType value;
} ret;
} MyTypeWrapper;
次に、呼び出された関数で:
MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
MyTypeWrapper wrapper;
// [...]
// If there is an error somewhere:
wrapper.status = ERROR;
wrapper.ret.errCode = MY_ERROR_CODE;
// Everything went well:
wrapper.status = SUCCESS;
wrapper.ret.value = myProcessedData;
return wrapper;
}
次の方法では、ラッパーのサイズがMyTypeに1バイト(ほとんどのコンパイラーで)を加えたものになるため、非常に有益です。 そして、スタックに別の引数をプッシュする必要はありません関数を呼び出すとき(両方の提示したメソッドでreturnedSize
またはreturnedError
)。
編集:最後のエラーにのみアクセスする必要があり、マルチスレッド環境で作業しない場合。
True/false(またはCで作業し、bool変数をサポートしていない場合は何らかの#define)のみを返すことができ、最後のエラーを保持するグローバルエラーバッファーがあります。
int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0
if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
MYAPI_ERROR* error = getLastError();
// error handling
}
私は、次の手法を使用したCでのエラー処理を好みます。
struct lnode *insert(char *data, int len, struct lnode *list) { struct lnode *p, *q; uint8_t good; struct { uint8_t alloc_node : 1; uint8_t alloc_str : 1; } cleanup = { 0, 0 }; // allocate node. p = (struct lnode *)malloc(sizeof(struct lnode)); good = cleanup.alloc_node = (p != NULL); // good? then allocate str if (good) { p->str = (char *)malloc(sizeof(char)*len); good = cleanup.alloc_str = (p->str != NULL); } // good? copy data if(good) { memcpy ( p->str, data, len ); } // still good? insert in list if(good) { if(NULL == list) { p->next = NULL; list = p; } else { q = list; while(q->next != NULL && good) { // duplicate found--not good good = (strcmp(q->str,p->str) != 0); q = q->next; } if (good) { p->next = q->next; q->next = p; } } } // not-good? cleanup. if(!good) { if(cleanup.alloc_str) free(p->str); if(cleanup.alloc_node) free(p); } // good? return list or else return NULL return (good ? list : NULL); }
2番目のアプローチでは、コンパイラーはより最適化されたコードを生成できます。これは、変数のアドレスが関数に渡されると、コンパイラーが他の関数への後続の呼び出し中にレジスターに値を保持できないためです。通常、完了コードは呼び出しの直後に1回だけ使用されますが、呼び出しから返された「実際の」データはより頻繁に使用されます。
他の優れた答えに加えて、エラーフラグとエラーコードを分離して、呼び出しごとに1行を保存することをお勧めします。
if( !doit(a, b, c, &errcode) )
{ (* handle *)
(* thine *)
(* error *)
}
多くのエラーチェックがある場合、この単純化は本当に役立ちます。