I've created simple try/catch macros that now I'd like to promote to wider use in my projects. I would have really liked to be able to do without global variables but I have not found any way to do it.
Any suggestion for improvement? Any issue that I may have overlooked?
#include <stdlib.h>
#include <setjmp.h>
#define utl_trymax 16
#ifdef UTL_MAIN
int utlErr = 0;
int utl_jbn = 0;
jmp_buf utl_jbv[utl_trymax];
#else
extern int utlErr;
extern int utl_jbn;
extern jmp_buf utl_jbv[utl_trymax];
#endif
#define utlTry for ( utlErr = -1 \
; utlErr == -1 && utl_jbn < utl_trymax \
; (utl_jbn> 0 ? utl_jbn-- : 0 ) , \
((utlErr > 0)? utlThrow(utlErr) : 0), \
(utlErr = 0) ) \
if ((utlErr = setjmp(utl_jbv[utl_jbn++])) == 0 )
#define utlCatch(e) else if ((utlErr == (e)) && ((utlErr = 0) == 0))
#define utlCatchAny else for ( ;utlErr > 0; utlErr = 0)
#define utlThrow(e) (utlErr=e, (utl_jbn > 0 && utlErr? \
longjmp(utl_jbv[utl_jbn-1], utlErr):\
exit(utlErr)))
The macros are to be used in this way:
utlTry {
// Exceptions can be thrown here or in
// any function called from here.
... utlThrow(EX_OUT_OF_MEM);
}
utlCatch(EX_OUT_OF_MEM) {
...
}
utlCatch(EX_DB_UNAVAILABLE) {
...
}
utlCatchAny {
... //optionally catch all other exceptions
}
if there's no handler for an exception, the program exits.
A simple example:
#include <stdio.h>
#define UTL_MAIN
#include "utltry.h"
void functhrow(int e) { utlThrow(e); }
int main(int argc, char *argv[])
{
int k = 0;
utlTry { functhrow(2); }
utlCatch(1) { k = 1; }
utlCatch(2) { k = 2; }
printf("Caught: %d%s\n",k,(k==2)?" (as expected)":"");
}
Here is another version I wrote to avoid the problems of having to use global variables. The price is to have to pass an extra argument to try
and throw
. I'd like to have comments on the code. The syntax is much closer to the C++ one
typedef struct utl_env_s {
jmp_buf jb;
struct utl_env_s *prev;
int err;
} utl_env_s, *tryenv;
#define utl_lblx(x,l) x##l
#define utl_lbl(x,l) utl_lblx(x,l)
#define try(utl_env) \
do { struct utl_env_s utl_cur_env; int utlErr; \
utl_cur_env.err = 0; \
utl_cur_env.prev = utl_env; \
utl_env = &utl_cur_env; \
for ( ; utl_cur_env.err >= 0 \
; (utl_env = utl_cur_env.prev) \
? utl_env->err = utl_cur_env.err : 0 ) \
if (utl_cur_env.err > 0) throw(utl_env,utl_cur_env.err); \
else if (!utl_env) break; \
else switch ((utl_cur_env.err = setjmp(utl_cur_env.jb))) {\
case 0 :
#define catch(e) break; \
case e : utlErr = utl_cur_env.err; \
utl_cur_env.err = -1; \
#define catchall break; \
default : utlErr = utl_cur_env.err; \
utl_cur_env.err = -1; \
#define tryend } \
} while(0)
#define throw(env,err) (env? longjmp(env->jb, err): exit(err))
I really dislike the tryend
part. I could get rid of it by forcing C99. Not sure if that would be the right thing to do ...
2 Answers 2
One problem with this is resource leaks. It is not that your macros leak, but that when you use them, you'll leak. For example, suppose you call your utlTry
in function a()
that then calls function b()
that allocates a resource (opens a file for example) and then calls function c()
that does something with the resource. If c()
fails and throws, the longjmp
back to a()
bypasses b()
where the resource could be freed. There's no way round this problem and just 'being careful' is unlikely to be enough.
Another problem is that the code doesn't look or behave like normal code. It looks as if I can add a statement between the try and the catch lines, but clearly I cannot. And there is no indication that the catch blocks are conditional. Perhaps this is nitpicking - if it is just you using the macros, I guess you'll get along fine.
Talking of nitpicking, there are some small issues:
- you mix naming styles, with camel-case
utlErr
etc and separate words inutl_jbn
etc utl_TRYMAX
might be expected to beUTL_TRYMAX
and if that limit is exceeded,utlTry
seems to skip its contained block.
Overall, I'd say, don't do it. Error handling the long way can be a pain, but taking shortcuts like this is likely to be worse in the long run (especially if you share your code).
-
\$\begingroup\$ Thanks for the comments, William. I'll surely change the
utl_TRYMAX
it should have beenutl_trymax
. As for theutl_xxxx' vs
utlAbcd', the convention is that first are for internal purpose only, while the lattere are supposed to be used by programmer. I should have said it in advance. \$\endgroup\$Remo.D– Remo.D2013年04月17日 19:44:50 +00:00Commented Apr 17, 2013 at 19:44
I'm sorry for my bluntness, but this code is absolutely horrible and unsafe. To worry about global variables while using a mess of function-like macros, together with setjmp
/longjmp
, is kind of like worrying about the paint of your car while smoke is raising from the engine and the brakes are dead.
Apart from that, trying to re-invent the C language is always a bad idea. Mainly because it confuses other C programmers.
You can implement exception handling in much safer and more readable ways:
typedef enum
{
OK,
ERR_THIS,
ERR_THAT
...
} err_t;
err_t func (void)
{
if(something)
return ERR_THIS;
if(something_else)
return ERR_THAT;
return OK;
}
int main(int argc, char *argv[])
{
switch(func())
{
case OK:
// code that should execute if no errors
break;
case ERR_THIS:
// handle error
break;
case ERR_THAT;
// handle error
break;
default: // equal to C++ catch(...)
}
}
And now you'll say "this is not exception handling!". Who cares? If you look at the generated machine code, this will generate exactly the same code as a C++ exception handling program. It will contain the same direct branches. The only difference is that the above program might contain less overhead and therefore execute quicker, with less memory usage.
-
\$\begingroup\$ Thanks for you comments, Lundin. Blunt feedback are exactly the reason I asked for a codereview. There would be little point otherwise :). That said, and just if you are interested, try to call some other function from func and assume there will be an error in that one. The point is that you need a way to propagate the error back to the first caller the cleanest way is to have all functions consitently returning an error code and build the rest around error code propagation. I confess I never managed to stay consistent with this style. \$\endgroup\$Remo.D– Remo.D2013年04月18日 12:31:37 +00:00Commented Apr 18, 2013 at 12:31
-
\$\begingroup\$ @Remo.D In a real scenario, you'd typically not want the errors to "fall all the way" back to the first caller. You'd probably want to catch 'em in the middle function, then pass them on. For example if the call stack is
main -> add_to_queue -> linked_list_add_node
, then the caller is not really interested in "Linked list exception, could not add node". They want a "queue full" error. \$\endgroup\$Lundin– Lundin2013年04月18日 19:54:21 +00:00Commented Apr 18, 2013 at 19:54 -
\$\begingroup\$ I'm not saying that the "same error" has to propagate back. In your example,
main
is interested in the fact that an error has occurred and you need a way to convey that message. Trust me, I've seen plenty of real scenarios where errors went unnoticed, program exited without releasing resources and so on. Exceptions are just a tool, I'm not advocating their use everywhere. There are cases where they come handy. I just wanted to have comments on my code, it was not my intention to start a debate on error handling (which, I believe, don't belong to "Code Review" exchange). \$\endgroup\$Remo.D– Remo.D2013年04月19日 07:05:08 +00:00Commented Apr 19, 2013 at 7:05 -
\$\begingroup\$ I love this answer and this is the strategy I typically use with my php code to avoid exceptions. \$\endgroup\$W.K.S– W.K.S2013年04月23日 23:35:48 +00:00Commented Apr 23, 2013 at 23:35
-
\$\begingroup\$ Your statement "this will generate exactly the same code as a C++ exception handling program" is not remotely true. C++ generates code with no branching inside
main
, which is the key performance advantage of using exceptions - you only pay for them when they happen, unlike error codes which cost an extra branch at every call site. \$\endgroup\$Eric– Eric2018年11月10日 01:15:41 +00:00Commented Nov 10, 2018 at 1:15
finally
clause doesn't seem to be executed if one throws an exception within CATCH (but I'll check again). Anyway, have you spotted a place in my code where there's a resource leaking? Or you just meant that, if one is not careful, using try/catch might lead to forgetting releasing acquired resources? \$\endgroup\$