Cコードでのエラー処理


152

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 ++固有のアイデアは、それが現時点では私にとってオプションではないため、例外が含まれていない限り、聞いてみるのも興味深いでしょう...


私はCを約2週間しか学習していませんが、OUTパラメーターは、構造体を値で返すオーバーヘッドを回避して軽減するため、大部分の関数のデフォルトの戻り値であると感じていますほとんどの変数はスタック上にあるため、メモリの割り当てを解除する必要があります。したがって、関数の実際の値に「return」を使用していないため、ほとんどの場合、エラー処理に自由に使用できます。
ジョエルロバーツ

回答:


74

戻り値としてエラーが好きです。APIを設計していて、ライブラリをできるだけ苦痛なく使用したい場合は、以下の追加について検討してください。

  • 考えられるすべてのエラー状態を1つのtypedefされたenumに格納し、libで使用します。intを返すだけでなく、さらに悪いことに、intまたはさまざまな列挙型をreturn-codeと組み合わせてください。

  • エラーを人間が読める形式に変換する関数を提供します。簡単にできます。エラー列挙型、const char *アウト。

  • このアイデアはマルチスレッドの使用を少し難しくすることを知っていますが、アプリケーションプログラマがグローバルエラーコールバックを設定できると便利です。これにより、バグハントセッション中にコールバックにブレークポイントを設定できるようになります。

それが役に立てば幸い。


5
なぜあなたは「この考えはマルチスレッドの使用を少し難しくする」と言います。マルチスレッド化が難しい部分はどこですか?簡単な例を挙げていただけますか?
SayeedHussain 2013年

1
@crypticcoder簡単に言うと、グローバルエラーコールバックは、どのスレッドコンテキストでも呼び出すことができます。エラーを出力するだけで問題は発生しません。問題を修正しようとすると、どの呼び出しスレッドがエラーの原因であるかを見つける必要があり、それが事態を困難にします。
Nils Pipenbrinck 2013年

9
エラーの詳細を伝えたい場合はどうしますか?たとえば、パーサーエラーがあり、構文エラーの行番号と列を提供し、それをすべてうまく表示する方法があるとします。
panzi 2013年

1
@panziでは、明らかに構造体を返し(または、構造体が本当に大きい場合はアウトポインターを使用する)、構造体を文字列としてフォーマットする関数が必要です。
ウィンガーセンドン2018

ここで、コードで最初の2つの箇条書きを示します。stackoverflow.com
Gabriel Staples

92

私は両方のアプローチを使用しましたが、どちらもうまくいきました。どちらを使用しても、私は常にこの原則を適用しようとします。

考えられるエラーがプログラマーエラーのみの場合は、エラーコードを返さず、関数内でアサートを使用します。

入力を検証するアサーションは、関数が期待することを明確に伝えますが、エラーチェックが多すぎるとプログラムロジックが不明瞭になる可能性があります。さまざまなエラーが発生した場合の対処方法を決定すると、設計が本当に複雑になる可能性があります。プログラマーがnullポインターを渡さないように主張できるのに、functionXがnullポインターをどのように処理する必要があるのか​​を理解するのはなぜですか?


1
Cでのアサートの例をお持ちですか?(私はCに対して非常に環境に
配慮しています

これは通常assert(X)、Xが真になりたい有効なCステートメントであるのと同じくらい簡単です。stackoverflow.com/q/1571340/10396を参照してください。
AShelly 2013年

14
ええと、絶対にライブラリコードでアサートを使用しないでください!また、コードの一片でのエラー処理の様々なスタイルを混在させないでください ...他の人のようでした
mirabilos

10
私はスタイルを混ぜないことについて確かに同意します。アサートに関するあなたの推論に興味があります。私の機能のドキュメントは、「引数XがNULLであってはなりません」または何がと間違っているよりも、「Yはこの列挙のメンバーでなければなりません」と言う場合assert(X!=NULL);またはassert(Y<enumtype_MAX);?これが正しい方法であると私が思う理由の詳細については、プログラマーへの回答とリンクされている質問を参照してください。
AShelly 2014年

8
@AShellyの問題は、リリースビルドには通常ないことを示しています。
カルマリウス

29

一般的なC(およびC ++)エラー処理手法のそれぞれをいつ使用するかについての推奨事項を含む、CMUのCERTからのスライドの素晴らしいセットがあります。最良のスライドの1つは、この決定木です。

エラー処理決定ツリー

このフローカートについて、私は2つのことを個人的に変更します。

まず、オブジェクトがエラーを示すために戻り値を使用する必要があることを明確にします。関数がオブジェクトからデータを抽出するだけでオブジェクトを変更しない場合、オブジェクト自体の整合性は危険にさらされておらず、戻り値を使用してエラーを示すことがより適切です。

次に、C ++で例外を使用することが常に適切であるとは限りません。例外は、エラー処理に費やされるソースコードの量を減らすことができ、ほとんどが関数のシグネチャに影響を与えず、コールスタックに渡すことができるデータに非常に柔軟性があるため、優れています。一方、いくつかの理由により、例外は適切な選択ではない場合があります。

  1. C ++例外には非常に特殊なセマンティクスがあります。これらのセマンティクスが必要ない場合は、C ++の例外を選択することはお勧めできません。例外はスローされた直後に処理する必要があり、設計では、エラーがコールスタックをいくつかのレベルまで巻き戻す必要がある場合を優先します。

  2. 例外をスローするC ++関数を後でラップして、例外をスローしないようにすることはできません。少なくとも、例外の全費用を払わなければ、例外は発生しません。エラーコードを返す関数は、C ++例外をスローするようにラップして、より柔軟にすることができます。C ++ newは、スローしないバリアントを提供することでこれを正しく実現しています。

  3. C ++の例外は比較的コストがかかりますが、この欠点は、例外を賢明に使用するプログラムにとっては大げさです。プログラムは、パフォーマンスが問題となるコードパスで例外をスローすべきではありません。プログラムがどれだけ速くエラーを報告して終了できるかは、実際には重要ではありません。

  4. C ++の例外が使用できない場合があります。それらは文字通りC ++実装では利用できないか、またはコードガイドラインにより禁止されています。


元の質問はマルチスレッドコンテキストに関するものだったので、ローカルエラーインジケーター手法(SirDarius回答で説明されているもの)は元の回答ではあまり評価されていなかったと思います。これはスレッドセーフであり、エラーが呼び出し元によって直ちに処理されることを強制せず、エラーを説明する任意のデータをバンドルできます。欠点は、オブジェクトによって保持される必要がある(または、何らかの形で外部に関連付けられていると思う)ことと、リターンコードよりも間違いなく無視しやすいことです。


5
GoogleのC ++コーディング標準では、まだC ++の例外は使用しない
Jonathan Leffler

19

ライブラリを作成するときは常に、最初の方法を使用します。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されたenumを使用すると、enum名をデバッガーで表示できます。これにより、ヘッダーファイルを常に参照する必要がなく、デバッグが容易になります。この列挙型を文字列に変換する関数を持つことも役立ちます。

使用するアプローチに関係なく最も重要な問題は、一貫性を保つことです。これは、関数と引数の命名、引数の順序付け、およびエラー処理に適用されます。


9

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;
}

2
コードの2番目のブロックは、回答の上部で参照されているFrancesco Niditoのページにある以前のバージョンのコードに基づいています。ETRYこの答えが書かれたので、コードが改訂されました。
Jonathan Leffler

2
Setjmpは恐ろしいエラー処理戦略です。コストがかかり、エラーが発生しやすく(変更された値を保持していない不揮発性の変更されたローカルなど)、setjmpとlongjmpの呼び出しの間にいずれかを割り当てると、リソースがリークします。sigjmp / longjmpのコストを回収する前に、30回の返品とint-valチェックを実行できるはずです。ほとんどのコールスタックは、特に再帰に重点を置かない場合(そして、そうする場合、戻り値+チェックのコスト以外のパフォーマンスの問題が発生します)、それほど深くなりません。
PSkocik

1
メモリをmallocしてからスローすると、メモリが永久にリークします。またsetjmp、コストがかかります。エラーがスローされない場合でも、CPU時間とスタックスペースをかなり消費します。Windowsでgccを使用する場合、C ++のさまざまな例外処理メソッドから選択できます。そのうちの1つは基本でsetjmpあり、実際にはコードを最大30%遅くします。
メッキー

7

私は個人的には前者のアプローチ(エラーインジケーターを返す)を好みます。

必要に応じて、返される結果はエラーが発生したことを示すだけで、正確なエラーを見つけるために別の関数が使用されます。

getSize()の例では、サイズは常にゼロまたは正でなければならないので、負の結果を返すと、UNIXシステムコールと同様にエラーを示す可能性があると考えます。

エラーオブジェクトをポインターとして渡して後者の方法を実行するために使用したライブラリーは考えられません。 stdioなどはすべて戻り値を使用します。


1
ちなみに、後者のアプローチを使用しているライブラリの1つはMayaプログラミングAPIです。Cではなくc ++ライブラリです。エラーの処理方法に一貫性がなく、エラーが戻り値として渡される場合と、結果が参照として渡される場合があります。
Laserallan 2008

1
最後の引数はエラーを示すためだけでなく、それも行います。
quinmars 2008

7

プログラムを作成するとき、通常、初期化中にエラー処理のためにスレッドをスピンオフし、ロックを含むエラーのために特別な構造を初期化します。次に、エラーを検出すると、戻り値を通じて、例外からの情報を構造体に入力し、SIGIOを例外処理スレッドに送信して、実行を続行できないかどうかを確認します。できない場合は、SIGURGを例外スレッドに送信し、プログラムを正常に停止します。


7

エラーコードを返すことは、Cでのエラー処理の通常のアプローチです。

しかし、最近では、発信エラーポインタアプローチも試しました。

これには、戻り値のアプローチに比べていくつかの利点があります。

  • より意味のある目的で戻り値を使用できます。

  • エラーパラメータを書き出す必要があるため、エラーを処理するか、または伝達する必要があります。(の戻り値をチェックすることを決して忘れfcloseませんね?)

  • エラーポインターを使用すると、関数を呼び出すときにエラーポインターを渡すことができます。関数のいずれかがそれを設定した場合、値は失われません。

  • エラー変数にデータブレークポイントを設定すると、最初にエラーが発生した場所をキャッチできます。条件付きブレークポイントを設定することで、特定のエラーもキャッチできます。

  • すべてのエラーを処理するかどうかのチェックを自動化するのが簡単になります。コードの規則により、エラーポインターを次のように呼び出すerr必要がある場合があります。これは最後の引数でなければなりません。したがって、スクリプトは文字列err);を照合し、その後にが続くかどうかを確認できif (*errます。実際には、実際にCER(check err return)とCEG(check err goto)というマクロを作成しました。したがって、エラーが発生した場合に常に入力する必要はなく、視覚的な混乱を減らすことができます。

ただし、コードのすべての関数にこの出力パラメーターがあるわけではありません。この発信パラメーターThingは、通常は例外をスローする場合に使用されます。


6

私は過去に多くのCプログラミングを行いました。そして、エラーコードの戻り値を本当に高く評価しました。ただし、いくつかの落とし穴があります。

  • エラー番号が重複しています。これは、グローバルなerrors.hファイルで解決できます。
  • エラーコードを確認するのを忘れて、これは手掛かりと長いデバッグ時間で解決されるべきです。しかし、最終的には学習します(または、他の誰かがデバッグを実行することがわかります)。

2
2番目の問題は、適切なコンパイラ警告レベル、適切なコードレビューメカニズム、および静的コードアナライザーツールによって解決できます。
イリヤ

1
原則に取り組むこともできます。API関数が呼び出され、戻り値がチェックされない場合は、バグがあります。
Jonathan Leffler

6

UNIXのアプローチは、2番目の提案に最も似ています。結果または単一の「間違った」値を返します。たとえば、openは成功するとファイル記述子を返し、失敗すると-1を返します。失敗した場合はerrno、外部グローバル整数も設定して、発生し失敗を示します。

その価値については、Cocoaも同様のアプローチを採用しています。多くのメソッドがBOOLを返し、NSError **パラメータを取得するため、失敗した場合はエラーが設定され、NOが返されます。次に、エラー処理は次のようになります。

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}

これは、2つのオプションの間のどこかにあります:-)。



5

これは私が面白いと思うアプローチですが、いくつかの規律を必要とします。

これは、ハンドル型変数がすべての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;
}

4

最初のアプローチはより良い私見です:

  • そのように関数を書く方が簡単です。関数の途中でエラーが発生した場合は、エラー値を返します。2番目のアプローチでは、パラメータの1つにエラー値を割り当ててから何かを返す必要があります。しかし、何を返しますか-正しい値がなく、エラー値を返しません。
  • それはより人気があるので、理解、維持がより簡単になります

4

私は間違いなく最初の解決策を好みます:

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エラー処理を正しく処理するためにいくつかの関数を呼び出せない限り、エラー処理コードとしてお勧めします。


3

エラーを返す代わりに、関数でデータを返すことを禁止する代わりに、戻り値の型にラッパーを使用することができます。

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提示した両方のメソッドで)、スタック別の引数をプッシュする必要はありません


3

ここに、ニルス・ピペンブリンクの答えの最初の2つの箇条書きを示す簡単なプログラムがあります。

彼の最初の2つの弾丸は次のとおりです。

  • 考えられるすべてのエラー状態を1つのtypedefされたenumに格納し、libで使用します。intを返すだけでなく、さらに悪いことに、intまたはさまざまな列挙型をreturn-codeと組み合わせてください。

  • エラーを人間が読める形式に変換する関数を提供します。簡単にできます。エラー列挙型、const char *アウト。

という名前のモジュールを作成したとしますmymoduleまず、mymodule.hで、列挙型ベースのエラーコードを定義し、これらのコードに対応するエラー文字列をいくつか記述します。ここでは、C文字列(char *)の配列を使用しています。これは、最初の列挙型エラーコードの値が0で、その後、数値を操作しない場合にのみ機能します。ギャップやその他の開始値を持つエラーコード番号を使用する場合は、マップされたC文字列配列の使用(下記を参照)から、switchステートメントまたはif / else ifステートメントを使用する関数の使用に変更する必要があります列挙型エラーコードから印刷可能なC文字列にマップします(これについては説明しません)。選択はあなた次第です。

mymodule.h

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.cには、列挙型エラーコードから印刷可能なC文字列にマッピングするためのマッピング関数が含まれています。

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.cには、いくつかの関数の呼び出しといくつかのエラーコードの出力を示すテストプログラムが含まれています。

main.c

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

出力:

C(またはC ++)の列挙型エラーコードのデモ
mymodule_func1()
からのエラーコード= MYMODULE_ERROR_OK
mymodule_func2()からのエラーコード= MYMODULE_ERROR_INVARG mymodule_func3()からのエラーコード= MYMODULE_ERROR_MYERROR

参照:

このコードは、https//onlinegdb.com/ByEbKLupSで自分で実行できます


2

これまでに述べたことに加えて、エラーコードを返す前に、エラーが返されたときにアサートまたは類似の診断を開始すると、トレースが非常に簡単になります。これを行う方法は、カスタマイズされたアサートを使用して、リリース時にコンパイルされますが、ソフトウェアが診断モードのときにのみ発生し、ログファイルに通知せずにレポートするか、画面上で一時停止するオプションがあります。

私は個人的にエラーコードをno_errorがゼロの負の整数として返しますが、次のバグの可能性があります

if (MyFunc())
 DoSomething();

別の方法として、失敗は常にゼロとして返され、LastError()関数を使用して実際のエラーの詳細を提供します。


2

私はこのQ&Aに何度も遭遇し、より包括的な回答を提供したいと考えました。これについて考える最良の方法は、呼び出し元にエラーを返す方法と、を返すです。

どうやって

関数から情報を返す方法は3つあります。

  1. 戻り値
  2. 引数外
  3. 帯域外、非ローカルgoto(setjmp / longjmp)、ファイルまたはグローバルスコープ変数、ファイルシステムなどが含まれます。

戻り値

戻り値は単一のオブジェクトのみですが、任意の複合体にすることができます。エラーを返す関数の例を次に示します。

  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の実用的な使用法を参照してください。

  1. インジケータ
  2. コード
  3. オブジェクト
  4. 折り返し電話

インジケータ

エラーインジケーターは、問題があることを通知するだけで、その問題の性質については何も通知しません。

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つは、ハッピーパスにオーバーヘッドがないエラーがない場合、コールバックを複数回、またはまったく呼び出せないことです。

ただし、制御の反転があります。呼び出しコードは、コールバックが呼び出されたかどうかを認識しません。そのため、インジケーターを使用することも意味があります。


1

編集:最後のエラーのみにアクセスする必要があり、マルチスレッド環境で作業しない場合。

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ではありませんが、OSによって提供されているかどうかはわかりません。たとえば、リアルタイムのオペレーティングシステムで作業している場合は、Cがありません。
イリヤ

1

2番目のアプローチでは、コンパイラーはより最適化されたコードを生成できます。これは、変数のアドレスが関数に渡されると、コンパイラーが他の関数への後続の呼び出し中にその値をレジスターに保持できないためです。完了コードは通常、呼び出しの直後に一度だけ使用されますが、呼び出しから返された「実際の」データはより頻繁に使用されます。


1

私は、次の手法を使用して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);
}

出典:http : //blog.staila.com/?p=114


1
良いテクニック。goto繰り返しの代わりにを使って、もっとすっきりしたものを見つけますif。参照:onetwo
Ant_222 2018年

0

他の素晴らしい答えに加えて、各呼び出しで1行を節約するために、エラーフラグとエラーコードを分離することをお勧めします。

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

多くのエラーチェックがある場合、この小さな単純化は本当に役立ちます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.