Cでcatchステートメントを試す


101

今日、他の言語に存在するtry / catchブロックについて考えていました。これはしばらくの間グーグルでしたが結果はありませんでした。私が知っていることから、Cにはtry / catchのようなものはありません。しかし、それらを「シミュレート」する方法はありますか?
もちろん、アサートやその他のトリックはありますが、発生した例外をキャッチするtry / catchのようなものはありません。ありがとうございました


3
例外のようなメカニズムは、スタックがほどかれたときに自動的にリソースを解放するメカニズムがなければ、一般的には役に立ちません。C ++はRAIIを使用します。Java、C#、Pythonなどは、ガベージコレクターを使用します。(ガベージコレクターはメモリのみを解放することに注意してください。他のタイプのリソースを自動的に解放するために、ファイナライザーやコンテキストマネージャーなども追加します...)
jamesdlin

@ jamesdlin、CでRAIIを実行できないのはなぜですか?
Pacerier

1
@Pacerier RAIIは、オブジェクトが破棄されるときに関数を自動的に呼び出す必要があります(つまり、デストラクタ)。Cでそれを行うことをどのように提案しますか?
jamesdlin

回答:


90

C自体は例外をサポートしていませんが、setjmpand longjmp呼び出しを使用して、例外をある程度シミュレートできます。

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened here\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjmp(s_jumpBuffer, 42);
}

このWebサイトには、setjmpおよびを使用して例外をシミュレートする方法に関する素晴らしいチュートリアルがありますlongjmp


1
素晴らしいソリューション!このソリューションはクロスですか?MSVC2012では機能しましたが、MacOSX Clangコンパイラでは機能しませんでした。
mannysz 2016

1
私の手がかり:私はtry catch句を使用すると、例外をキャッチできると思いました(ゼロによる除算など)。この関数では、自分でスローした例外のみをキャッチできるようです。longjmpを呼び出しても実際の例外はスローされませんか?このコードを使用して、try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')}; 正しく機能しないようなことをした場合はどうなりますか?
サム・

Devide by zeroはC ++でも例外ではありません。それを処理するには、除数が0でないことを確認して処理するか、devide by zero式を実行したときにスローされるSIGFPEを処理する必要があります。
James

25

同様のエラー処理状況では、Cでgotoを使用します。
これは、Cで取得できる例外に最も近いものです。


3
@JensGustedtこれは、gotoが現在非常に頻繁に使用されているものであり、意味のある例です(setjmp / ljmpがより良い代替手段ですが、通常はlabel + gotoがより多く使用されます)。
Tomas Pruzina 2013

1
@AoeAoe、おそらくgotoエラー処理により多く使用されますが、何ですか?問題は、そのようなエラー処理についてではなく、try / catchの同等物について明示的にです。goto同じ関数に制限されているため、try / catchと同等ではありません。
イェンスガステッド2013

@JensGustedt私はgotoとそれを使用する人々の憎しみ/恐怖に少し反応しました(私の先生も私にgotoの使用法について怖い話を大学で教えてくれました)。[OT] gotoについて本当に、本当にリスクがあり、「曇っている」ことは、「後方に移動する」ことだけですが、Linux VFSでそれを確認しました(Git Blameの人は、パフォーマンスが非常に有益であると誓いました)。
Tomas Pruzina 2013

広くgoto受け入れられている最新のピアレビュー済みソースで使用されているtry / catchメカニズムとしての正当な使用については、systemctlソースを参照してください。goto「投げ」に相当するものとfinish「キャッチ」に相当するものを検索します。
スチュワート

13

わかりました、これに答えるのを我慢できませんでした。まずCでこれをシミュレートするのは良い考えではないと思います。これは、実際にはCにとって外国の概念であるためです。

私たちはすることができます使用し、使用にC ++のtry /スロー/キャッチの限定バージョンを与えるために、プリプロセッサとローカルスタック変数を乱用します。

バージョン1(ローカルスコープスロー)

#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

バージョン1はローカルスローのみです(関数のスコープを離れることはできません)。これは、コード内で変数を宣言するC99の機能に依存します(関数の最初の試みがC89で機能する必要があります)。

この関数はローカル変数を作成するだけなので、エラーがあったかどうかを判断し、gotoを使用してcatchブロックにジャンプします。

例えば:

#include <stdio.h>
#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

int main(void)
{
    try
    {
        printf("One\n");
        throw();
        printf("Two\n");
    }
    catch(...)
    {
        printf("Error\n");
    }
    return 0;
}

これは次のようなものになります:

int main(void)
{
    bool HadError=false;
    {
        printf("One\n");
        HadError=true;
        goto ExitJmp;
        printf("Two\n");
    }
ExitJmp:
    if(HadError)
    {
        printf("Error\n");
    }
    return 0;
}

バージョン2(スコープジャンプ)

#include <stdbool.h>
#include <setjmp.h>

jmp_buf *g__ActiveBuf;

#define try jmp_buf __LocalJmpBuff;jmp_buf *__OldActiveBuf=g__ActiveBuf;bool __WasThrown=false;g__ActiveBuf=&__LocalJmpBuff;if(setjmp(__LocalJmpBuff)){__WasThrown=true;}else
#define catch(x) g__ActiveBuf=__OldActiveBuf;if(__WasThrown)
#define throw(x) longjmp(*g__ActiveBuf,1);

バージョン2はもっと複雑ですが、基本的には同じように機能します。これは、現在の関数からtryブロックへの長いジャンプを使用します。次に、tryブロックはif / elseを使用してコードブロックをスキップしてcatchブロックに進み、ローカル変数をチェックしてキャッチするかどうかを確認します。

例は再び拡張されました:

jmp_buf *g_ActiveBuf;

int main(void)
{
    jmp_buf LocalJmpBuff;
    jmp_buf *OldActiveBuf=g_ActiveBuf;
    bool WasThrown=false;
    g_ActiveBuf=&LocalJmpBuff;

    if(setjmp(LocalJmpBuff))
    {
        WasThrown=true;
    }
    else
    {
        printf("One\n");
        longjmp(*g_ActiveBuf,1);
        printf("Two\n");
    }
    g_ActiveBuf=OldActiveBuf;
    if(WasThrown)
    {
        printf("Error\n");
    }
    return 0;
}

これはグローバルポインターを使用するため、longjmp()は最後に実行された試行を認識します。私たちはしている使用して、子機能もtry / catchブロックを持つことができますので、スタックを悪用します。

このコードを使用すると、いくつかの欠点があります(ただし、楽しいメンタルエクササイズです)。

  • 呼び出されているデコンストラクターがないため、割り当てられたメモリは解放されません。
  • スコープ内に複数のtry / catchを含めることはできません(ネストなし)
  • C ++のように、例外やその他のデータを実際にスローすることはできません
  • スレッドセーフではありません
  • 他のプログラマーは、ハッキングに気付かず、C ++のtry / catchブロックのように使用しようとするため、失敗するように設定しています。

素敵な代替ソリューション。
HaseeB Mir 2018

バージョン1は素晴らしいアイデアですが、その__HadError変数をリセットするか、スコープを設定する必要があります。そうしないと、同じブロックで複数のtry-catchを使用できなくなります。多分のようなグローバル関数を使用しますbool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}。しかし、ローカル変数も再定義されるため、状況は少し手に負えなくなります。
flamewave000

はい、同じ関数内の1つのtry-catchに制限されています。ただし、同じ関数で重複したラベルを使用することはできないため、大きな問題は変数です。
ポールハチンソン

10

C99では、非ローカル制御フローにsetjmp/ longjmpを使用できます。

単一のスコープ内では、この例のgotoよう、複数のリソース割り当てと複数の出口が存在する場合のCの一般的な構造化コーディングパターンが使用さます。これは、C ++が内部で自動オブジェクトのデストラクタコールを実装する方法に似ており、これをこまめに使用すると、複雑な関数でもある程度のクリーンさを実現できます。


5

他の回答のいくつかはsetjmpおよびを使用した単純なケースをカバーしていlongjmpますが、実際のアプリケーションでは、本当に重要な2つの懸念があります。

  1. try / catchブロックのネスト。単一のグローバル変数を使用するjmp_bufと、これらが機能しなくなります。
  2. スレッディング。あなたのための単一のグローバル変数はjmp_buf、この状況であらゆる種類の苦痛を引き起こします。

これらの解決策は、jmp_buf進行中に更新されるスレッドローカルスタックを維持することです。(これはluaが内部で使用するものだと思います)。

だからこれの代わりに(JaredParの素晴らしい答えから)

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjump(s_jumpBuffer, 42);
}

あなたは次のようなものを使うでしょう:

#define MAX_EXCEPTION_DEPTH 10;
struct exception_state {
  jmp_buf s_jumpBuffer[MAX_EXCEPTION_DEPTH];
  int current_depth;
};

int try_point(struct exception_state * state) {
  if(current_depth==MAX_EXCEPTION_DEPTH) {
     abort();
  }
  int ok = setjmp(state->jumpBuffer[state->current_depth]);
  if(ok) {
    state->current_depth++;
  } else {
    //We've had an exception update the stack.
    state->current_depth--;
  }
  return ok;
}

void throw_exception(struct exception_state * state) {
  longjump(state->current_depth-1,1);
}

void catch_point(struct exception_state * state) {
    state->current_depth--;
}

void end_try_point(struct exception_state * state) {
    state->current_depth--;
}

__thread struct exception_state g_exception_state; 

void Example() { 
  if (try_point(&g_exception_state)) {
    catch_point(&g_exception_state);
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
    end_try_point(&g_exception_state);
  }
}

void Test() {
  // Rough equivalent of `throw`
  throw_exception(g_exception_state);
}

繰り返しますが、これのより現実的なバージョンには、エラー情報をに格納するいくつかの方法が含まれexception_state、より適切に処理されますMAX_EXCEPTION_DEPTH(おそらくreallocを使用してバッファーを拡張するなど)。

免責事項:上記のコードはまったくテストせずに記述されています。それは純粋にあなたが物事をどのように構成するかについてのアイデアを得るのです。システムやコンパイラが異なれば、スレッドローカルストレージの実装方法も異なります。コードには、コンパイルエラーとロジックエラーの両方が含まれている可能性があります。選択したとおりに自由に使用できますが、使用する前にテストしてください;)


4

簡単なグーグル検索は、他の人が述べたように、setjmp / longjmpを使用するこのような厄介なソリューションを生み出します。C ++ / Javaのtry / catchほど簡単でエレガントなものはありません。私はエイダの自分自身の例外処理にかなり偏っています。

ifステートメントですべてを確認してください:)



2

これは、Cでエラー処理を行うもう1つの方法であり、setjmp / longjmpを使用するよりもパフォーマンスが高くなります。残念ながら、MSVCでは機能しませんが、GCC / Clangのみを使用するオプションがある場合は、それを検討することもできます。具体的には、「値としてのラベル」拡張機能を使用します。これにより、ラベルのアドレスを取得して値に格納し、無条件にそれにジャンプできます。例を使用してそれを提示します。

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    /* Declare an error handler variable. This will hold the address
       to jump to if an error occurs to cleanup pending resources.
       Initialize it to the err label which simply returns an
       error value (NULL in this example). The && operator resolves to
       the address of the label err */
    void *eh = &&err;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    if (!engine)
        goto *eh; /* this is essentially your "throw" */

    /* Now make sure that if we throw from this point on, the memory
       gets deallocated. As a convention you could name the label "undo_"
       followed by the operation to rollback. */
    eh = &&undo_malloc;

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    if (!engine->window)
        goto *eh;   /* The neat trick about using approach is that you don't
                       need to remember what "undo" label to go to in code.
                       Simply go to *eh. */

    eh = &&undo_window_open;

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}

必要に応じて、defineの共通コードをリファクタリングして、独自のエラー処理システムを効果的に実装できます。

/* Put at the beginning of a function that may fail. */
#define declthrows void *_eh = &&err

/* Cleans up resources and returns error result. */
#define throw goto *_eh

/* Sets a new undo checkpoint. */
#define undo(label) _eh = &&undo_##label

/* Throws if [condition] evaluates to false. */
#define check(condition) if (!(condition)) throw

/* Throws if [condition] evaluates to false. Then sets a new undo checkpoint. */
#define checkpoint(label, condition) { check(condition); undo(label); }

次に、例は

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    declthrows;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    checkpoint(malloc, engine);

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    checkpoint(window_open, engine->window);

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}

2

警告:以下はあまり良くありませんが、うまくいきます。

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    unsigned int  id;
    char         *name;
    char         *msg;
} error;

#define _printerr(e, s, ...) fprintf(stderr, "\033[1m\033[37m" "%s:%d: " "\033[1m\033[31m" e ":" "\033[1m\033[37m" " ‘%s_error’ " "\033[0m" s "\n", __FILE__, __LINE__, (*__err)->name, ##__VA_ARGS__)
#define printerr(s, ...) _printerr("error", s, ##__VA_ARGS__)
#define printuncaughterr() _printerr("uncaught error", "%s", (*__err)->msg)

#define _errordef(n, _id) \
error* new_##n##_error_msg(char* msg) { \
    error* self = malloc(sizeof(error)); \
    self->id = _id; \
    self->name = #n; \
    self->msg = msg; \
    return self; \
} \
error* new_##n##_error() { return new_##n##_error_msg(""); }

#define errordef(n) _errordef(n, __COUNTER__ +1)

#define try(try_block, err, err_name, catch_block) { \
    error * err_name = NULL; \
    error ** __err = & err_name; \
    void __try_fn() try_block \
    __try_fn(); \
    void __catch_fn() { \
        if (err_name == NULL) return; \
        unsigned int __##err_name##_id = new_##err##_error()->id; \
        if (__##err_name##_id != 0 && __##err_name##_id != err_name->id) \
            printuncaughterr(); \
        else if (__##err_name##_id != 0 || __##err_name##_id != err_name->id) \
            catch_block \
    } \
    __catch_fn(); \
}

#define throw(e) { *__err = e; return; }

_errordef(any, 0)

使用法:

errordef(my_err1)
errordef(my_err2)

try ({
    printf("Helloo\n");
    throw(new_my_err1_error_msg("hiiiii!"));
    printf("This will not be printed!\n");
}, /*catch*/ any, e, {
    printf("My lovely error: %s %s\n", e->name, e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err2_error_msg("my msg!"));
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printerr("%s", e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err1_error());
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printf("Catch %s if you can!\n", e->name);
})

出力:

Helloo
My lovely error: my_err1 hiiiii!

Helloo
/home/naheel/Desktop/aa.c:28: error: my_err2_error my msg!

Helloo
/home/naheel/Desktop/aa.c:38: uncaught error: my_err1_error 

これはネストされた関数とを使用していることに注意してください__COUNTER__。gccを使用している場合は安全です。


1

Redisはgotoを使用してtry / catchをシミュレートします。IMHOは非常にクリーンでエレガントです。

/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success. */
int rdbSave(char *filename) {
    char tmpfile[256];
    FILE *fp;
    rio rdb;
    int error = 0;

    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
            strerror(errno));
        return REDIS_ERR;
    }

    rioInitWithFile(&rdb,fp);
    if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    redisLog(REDIS_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = REDIS_OK;
    return REDIS_OK;

werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    return REDIS_ERR;
}

コードが壊れています。errno失敗したシステムコールの直後にのみ使用し、その後の3つのコールでは使用しないでください。
2015

このコードは、エラー処理ロジックを複数の場所で複製し、fclose(fp)を複数回呼び出すなどの誤った処理を行う可能性があります。複数のラベルを使用し、それらのラベルを使用して再利用する必要があるものを(すべてのエラーに対して1つではなく)エンコードしてから、コード内のエラーが発生した場所に応じて適切なエラー処理スポットにジャンプする方がはるかに良いでしょう。
jschultz410

1

Cでは、明示的なエラー処理のためにif + gotoを手動で使用して、自動「オブジェクト再利用」とともに例外を「シミュレート」できます。

私はよく次のようなCコードを記述します(エラー処理を強調するために煮詰めています)。

#include <assert.h>

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    if ( ( ret = foo_init( f ) ) )
        goto FAIL;

    if ( ( ret = goo_init( g ) ) )
        goto FAIL_F;

    if ( ( ret = poo_init( p ) ) )
        goto FAIL_G;

    if ( ( ret = loo_init( l ) ) )
        goto FAIL_P;

    assert( 0 == ret );
    goto END;

    /* error handling and return */

    /* Note that we finalize in opposite order of initialization because we are unwinding a *STACK* of initialized objects */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

これは完全に標準のANSI Cであり、メインラインコードからエラー処理を分離し、C ++と同じように初期化されたオブジェクトの(手動の)スタックアンワインドを可能にします。各ポイントで障害を明示的にテストしているので、エラーが発生する可能性のある場所ごとに特定のロギングまたはエラー処理を挿入することが容易になります。

小さなマクロの魔法を気にしない場合は、スタックトレースを使用してエラーをログに記録するなど、他のことをしながら、これをより簡潔にすることができます。例えば:

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define TRY( X, LABEL ) do { if ( ( X ) ) { fprintf( stderr, "%s:%d: Statement '" #X "' failed! %d, %s\n", __FILE__, __LINE__, ret, strerror( ret ) ); goto LABEL; } while ( 0 )

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    TRY( ret = foo_init( f ), FAIL );
    TRY( ret = goo_init( g ), FAIL_F );
    TRY( ret = poo_init( p ), FAIL_G );
    TRY( ret = loo_init( l ), FAIL_P );

    assert( 0 == ret );
    goto END;

    /* error handling and return */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

もちろん、これはC ++の例外+デストラクタほどエレガントではありません。たとえば、この方法で1つの関数内に複数のエラー処理スタックをネストすることは、あまりクリーンではありません。代わりに、これらを同様にエラーを処理する自己完結型のサブ関数に分割し、初期化+ファイナライズをこのように明示的に行います。

これも単一の関数内でのみ機能し、より高いレベルの呼び出し元が同様の明示的なエラー処理ロジックを実装しない限り、スタックをジャンプし続けませんが、C ++例外は、適切なハンドラーが見つかるまでスタックをジャンプし続けます。また、任意の型をスローすることもできませんが、エラーコードのみをスローします。

このように体系的にコーディングする(つまり、単一のエントリと単一の終了ポイントを使用する)ことで、実行前と実行後(「最終的に」)のロジックを非常に簡単に挿入できます。ENDラベルの後に「最終」ロジックを置くだけです。


1
非常に素晴らしい。私は似たようなことをする傾向があります。gotoはこのシナリオに最適です。唯一の違いは、最後の「goto END」の必要性が見当たらないことです。その時点で成功リターンを挿入し、残りの後に失敗リターンを挿入します。
Neil Roy、

1
おかげで@NeilRoy goto ENDの理由は、関数の大部分が単一のエントリポイントと単一の出口ポイントを持つことが好きだからです。そうすれば、「最終的な」ロジックを任意の関数に追加したい場合、どこかに潜んでいる他の隠された戻りがあることを心配する必要なく、常に簡単にできます。:)
jschultz410

0

Win32でCを使用している場合は、その構造化例外処理(SEH)を利用して、try / catchをシミュレートできます。

setjmp()およびをサポートしていないプラットフォームでCを使用している場合longjmp()は、このpjsipライブラリの例外処理を参照してください。独自の実装が提供されます。


-1

おそらく主要な言語ではないかもしれませんが(残念ながら)、APLには(EA操作(Execute Alternateの略)があります。

使用法: 'Y'⎕EA 'X'ここで、XとYは、文字列または関数名として提供されるコードスニペットです。

Xでエラーが発生した場合は、代わりにY(通常はエラー処理)が実行されます。


2
こんにちはmappo、StackOverflowへようこそ。興味深いことですが、質問は特にCでこれを行うことに関するものでした。したがって、これは実際には質問の答えに
luser droog 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.