web-dev-qa-db-ja.com

すでに値を返しているときに関数からエラーを返す最良の方法は何ですか?

文字列を整数に変換して整数を返す関数をCで作成しました。関数を呼び出すときに、文字列が有効な数値でないかどうかも通知してもらいたいです。以前は、文字列を負の数に変換する必要がなかったため、このエラーが発生したときに-1を返しました。しかし、文字列を負の数に変換したいので、エラーを報告する最良の方法は何ですか?

これについてはっきりしなかった場合:この関数がユーザーにエラーを報告したくないので、関数を呼び出したコードにエラーを報告したいと思います。 (「レポート」は使用するのに間違った単語である可能性があります...)

コードは次のとおりです。

s32 intval(const char *string) {
    bool negative = false;
    u32 current_char = 0;

    if (string[0] == '-') {
        negative = true;
        current_char = 1;
    }

    s32 num = 0;
    while (string[current_char]) {
        if (string[current_char] < '0' || string[current_char] > '9') {
            // Return an error here.. but how?
        }

        num *= 10;
        num += string[current_char] - '0';
        current_char++;
    }

    if (negative) {
        num = -num;
    }

    return num;
}
28
Jeremy Ruten

いくつかの方法があります。すべてに長所と短所があります。

  • 関数にエラーコードを返し、結果を返す場所へのポインターを渡すようにします。これの良いところは、結果のオーバーロードがないことです。悪い点は、関数の実際の結果を式で直接使用できないことです。

    Evan Teranが提案 呼び出し元が成功変数へのポインターを渡し(呼び出し元が気にしない場合はオプションでNULLにすることができます)、関数から実際の値を返す、このバリエーション。これには、呼び出し元がエラー結果のデフォルト値でOKである場合、または関数が失敗しないことがわかっている場合に、関数を式で直接使用できるという利点があります。

  • 特別な「センチネル」戻り値を使用して、負の数(通常の戻り値を負にできない場合)、_INT_MAX_または_INT_MIN_などのエラーを示します。より詳細なエラー情報を取得するには、別の関数(GetLastError()など)またはグローバル変数(errnoなど)の呼び出しを調べる必要がある場合があります。これは、戻り値に無効な値がない場合はうまく機能せず、多くの人が一般的に悪い形式と見なしています。

    この手法を使用する関数の例はgetc()で、ファイルの終わりに到達した場合、またはエラーが発生した場合にEOF)を返します。

  • 関数がエラー表示を直接返さないようにしますが、呼び出し元が別の関数またはグローバルを照会する必要があります。これは、VBの「_On Error Goto Next_」モードの動作に似ています。これは、ほとんどの場合、悪い方法と考えられています。

  • さらに別の方法は、「デフォルト」値を設定することです。たとえば、atoi()関数とほぼ同じ機能を持つintval()関数は、文字を変換できない場合に0を返します(その点で関数とは異なります)。文字列の終わりまたは数字ではない文字に到達するまで、変換する文字を消費します)。

    ここでの明らかな欠点は、実際の値が変換されたかどうか、またはジャンクがatoi()に渡されたかどうかを判断するのが難しい場合があることです。

    私はエラーを処理するこの方法の大ファンではありません。

他のオプションが頭に浮かんだら更新します...

35
Michael Burr

.NETが Int32.TryParse でこれを処理する方法は、成功/失敗を返し、解析された値を参照渡しパラメーターで返すことです。同じことがCにも当てはまります。

int intval(const char *string, s32 *parsed)
{
    *parsed = 0; // So that if we return an error, the value is well-defined

    // Normal code, returning error codes if necessary
    // ...

    *parsed = num;
    return SUCCESS; // Or whatever
}
16
Jon Skeet

一般的な方法は、次のような成功フラグへのポインタを渡すことです。

int my_function(int *ok) {
    /* whatever */
    if(ok) {
        *ok = success;
    }
    return ret_val;
}

このように呼んでください:

int ok;
int ret = my_function(&ok);
if(ok) {
    /* use ret safely here */
}

編集:ここでの実装例:

s32 intval(const char *string, int *ok) {
    bool negative = false;
    u32 current_char = 0;

    if (string[0] == '-') {
        negative = true;
        current_char = 1;
    }

    s32 num = 0;
    while (string[current_char]) {
        if (string[current_char] < '0' || string[current_char] > '9') {
                // Return an error here.. but how?
                if(ok) { *ok = 0; }
        }

        num *= 10;
        num += string[current_char] - '0';
        current_char++;
    }

    if (negative) {
        num = -num;
    }
    if(ok) { *ok = 1; }
    return num;
}

int ok;
s32 val = intval("123a", &ok);
if(ok) {
    printf("conversion successful\n");
}
11
Evan Teran

Osスタイルのグローバルerrno変数も人気があります。使用する errno.h

Errnoがゼロ以外の場合、問題が発生しています。

errno のmanページリファレンスは次のとおりです。

6
S.Lott

標準ライブラリがこの問題をどのように処理するかを見てみましょう。

long  strtol(const  char  * restrict str,  char **restrict endptr, int base);

ここで、呼び出し後、endptrは解析できなかった最初の文字を指します。 endptr == strの場合、文字は変換されませんでした。これは問題です。

4
Arkadiy

一般的に、私はJonSkeetが提案した方法を好みます。成功に関するブール値(intまたはuint)を返し、渡されたアドレスに結果を格納します。ただし、関数はstrtolと非常に似ているため、関数に同じ(または類似の)APIを使用することをお勧めします。 my_strtos32のような同様の名前を付けると、ドキュメントを読まなくても関数の機能を簡単に理解できます。

編集:関数は明示的に10ベースであるため、my_strtos32_base10の方が適切な名前です。関数がボトルネックでない限り、実装をスキップできます。そして、単にstrtolをラップアラウンドします。


s32
my_strtos32_base10(const char *nptr, char **endptr)
{
    long ret;
    ret = strtol(nptr, endptr, 10);
    return ret;
}

後でそれがボトルネックであることに気付いた場合でも、ニーズに合わせて最適化することができます。

3
quinmars

すでに値を返しているときに関数からエラーを返す最良の方法は何ですか?

さまざまな答えに対するいくつかの追加の考え。


構造体を返す

コードは値とエラーコードを返すことができます。懸念はタイプの急増です。

_typedef struct {
  int value;
  int error;
} int_error;

int_error intval(const char *string);

...

int_error = intval(some_string);
if (int_error.error) {
  Process_Error();
}

int only_care_about_value = intval(some_string).value;
int only_care_about_error = intval(some_string).error;
_

非数およびNULL

関数の戻り値の型が提供する場合は、特別な値を使用します。
Cは非数を必要としませんが、どこにでもあります。

_#include <math.h>
#include <stddef.h>

double y = foo(x);
if (isnan(y)) {
  Process_Error();
}

void *ptr = bar(x);
if (ptr == NULL) {
  Process_Error();
}
_

__Generic_ /関数のオーバーロード

error_t foo(&dest, x)dest_t foo(x, &error)の長所と短所を考慮して、

__Generic_のカスケード使用、またはコンパイラ拡張としての関数オーバーロードを使用して、2つ以上の型を選択すると、戻り値ではなく、呼び出しのパラメータに基づいて呼び出される基になる関数を区別するのが理にかなっています。一般的なタイプであるエラーステータスを返します。

例:1つの型の値を_long long_からshortのように、より狭い型に変換し、ソースかどうかをテストする関数error_t narrow(destination_t *, source_t) valueはターゲットtypeの範囲内にありました。

_long long ll = ...; 
int i;
char ch; 
error = narrow(&i, ll);
...
error = narrow(&ch, i);
_
1
chux

プロパティが対象の値であるクラスのインスタンスを返すか、別のプロパティが何らかのステータスフラグであるかのいずれかを返すことができます。または、結果クラスのインスタンスを渡します。

Pseudo code
  MyErrStatEnum = (myUndefined, myOK, myNegativeVal, myWhatever)

ResultClass
  Value:Integer;
  ErrorStatus:MyErrStatEnum

例1:

result := yourMethod(inputString)

if Result.ErrorStatus = myOK then 
   use Result.Value
else
  do something with Result.ErrorStatus

free result

例2

create result
yourMethod(inputString, result)

if Result.ErrorStatus = myOK then 
   use Result.Value
else
  do something with Result.ErrorStatus

free result

このアプローチの利点は、Resultクラスにプロパティを追加することで、いつでも戻ってくる情報を拡張できることです。

この概念をさらに拡張するために、複数の入力パラメーターを持つメソッド呼び出しにも適用されます。たとえば、CallYourMethod(val1、val2、val3、bool1、bool2、string1)の代わりに、val1、val2、val3、bool1、bool2、string1に一致するプロパティを持つクラスを作成し、それを単一の入力パラメーターとして使用します。メソッド呼び出しをクリーンアップし、将来コードをより簡単に変更できるようにします。いくつかのパラメーターを使用したメソッド呼び出しは、使用/デバッグがはるかに難しいことをご存知だと思います。 (7は私が言う絶対的な最もです。)

0
Darian Miller