今日、他の言語に存在するtry / catchブロックについて考えていました。これはしばらくの間グーグルでしたが結果はありませんでした。私が知っていることから、Cにはtry / catchのようなものはありません。しかし、それらを「シミュレート」する方法はありますか?
もちろん、アサートやその他のトリックはありますが、発生した例外をキャッチするtry / catchのようなものはありません。ありがとうございました
今日、他の言語に存在するtry / catchブロックについて考えていました。これはしばらくの間グーグルでしたが結果はありませんでした。私が知っていることから、Cにはtry / catchのようなものはありません。しかし、それらを「シミュレート」する方法はありますか?
もちろん、アサートやその他のトリックはありますが、発生した例外をキャッチするtry / catchのようなものはありません。ありがとうございました
回答:
C自体は例外をサポートしていませんが、setjmp
and 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
try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')};
正しく機能しないようなことをした場合はどうなりますか?
同様のエラー処理状況では、Cでgotoを使用します。
これは、Cで取得できる例外に最も近いものです。
goto
エラー処理により多く使用されますが、何ですか?問題は、そのようなエラー処理についてではなく、try / catchの同等物について明示的にです。goto
同じ関数に制限されているため、try / catchと同等ではありません。
goto
受け入れられている最新のピアレビュー済みソースで使用されているtry / catchメカニズムとしての正当な使用法については、systemctlソースを参照してください。goto
「投げ」に相当するものとfinish
「キャッチ」に相当するものを検索します。
わかりました、これに答えるのを我慢できませんでした。まず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ブロックを持つことができますので、スタックを悪用します。
このコードを使用すると、いくつかの欠点があります(ただし、楽しいメンタルエクササイズです)。
bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}
。しかし、ローカル変数も再定義されるため、状況は少し手に負えなくなります。
他の回答のいくつかはsetjmp
およびを使用した単純なケースをカバーしていlongjmp
ますが、実際のアプリケーションでは、本当に重要な2つの懸念があります。
jmp_buf
と、これらが機能しなくなります。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を使用してバッファーを拡張するなど)。
免責事項:上記のコードはまったくテストせずに記述されています。それは純粋にあなたが物事をどのように構成するかについてのアイデアを得るのです。システムやコンパイラが異なれば、スレッドローカルストレージの実装方法も異なります。コードには、コンパイルエラーとロジックエラーの両方が含まれている可能性があります。選択したとおりに自由に使用できますが、使用する前にテストしてください;)
これはsetjmp/longjmp
C で行うことができます。P99には、このための非常に快適なツールセットがあり、C11の新しいスレッドモデルとも一致しています。
これは、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;
}
警告:以下はあまり良くありませんが、うまくいきます。
#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を使用している場合は安全です。
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つのコールでは使用しないでください。
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ラベルの後に「最終」ロジックを置くだけです。
Win32でCを使用している場合は、その構造化例外処理(SEH)を利用して、try / catchをシミュレートできます。
setjmp()
およびをサポートしていないプラットフォームでCを使用している場合longjmp()
は、このpjsipライブラリの例外処理を参照してください。独自の実装が提供されます。
おそらく主要な言語ではないかもしれませんが(残念ながら)、APLには(EA操作(Execute Alternateの略)があります。
使用法: 'Y'⎕EA 'X'ここで、XとYは、文字列または関数名として提供されるコードスニペットです。
Xでエラーが発生した場合は、代わりにY(通常はエラー処理)が実行されます。