i often find adding debugs, ruins the readability of the code.
i've checked many projects, where in often each debug statement
spans 7 to 8 lines, which makes the code look ugly and even
incomprehensible. so here's my question, are there any techniques
that can be used to add debugs in a clean manner. i know it sound
not much like a C related question at first, but, i am looking if
c-lang has anything to offer in this regard.
One solution is to abstract the behavior of a debug or error checking
statement and use macros to perform the code generation.
For example, in many of the functions I design, I have macros that
validate the parameters in a defensive manner.
\code (inspired from GLib)
#if !defined(C_NO_CONSTRAINTS)
#define c_return_if_fail( expr )
\
do
\
{ \
if ( expr ) {}
\
else
\
{ \
gc_signal_constraint_violation( c_constraint_violation, #expr,
\
__FILE__, __LINE__, __func__ );
\
return;
\
}
\
} while (0)
#define c_return_value_if_fail( expr, val )
\
do
\
{ \
if ( expr ) {}
\
else
\
{ \
gc_signal_constraint_violation( c_constraint_violation, #expr,
\
__FILE__, __LINE__, __func__ );
\
return (val);
\
}
\
} while (0)
#else
#define c_return_if_fail( expr ) \
do \
{ \
(void)0; \
} while (0)
#define c_return_value_if_fail( expr, val ) \
do \
{ \
(void)0; \
} while (0)
#endif /* C_NO_CONSTRAINTS */
void gc_signal_constraint_violation( c_constraint_violation_type type,
const char* expr,
const char* file,
int line,
const char* func )
{
static const char* prefix[] = { "Constraint violation",
"Unexpected condition" };
if ( *func != '\0' )
{
(void)fprintf( stderr, "%s: %s, file %s, line %d, function %s\n",
prefix[type], expr, file, line, func );
}
else
{
(void)fprintf( stderr, "%s: %s, file %s, line %d\n",
prefix[type], expr, file, line );
}
fflush( stderr );
}
\endcode
With this infrastructure, it becomes more readable to integrate
parameter validation without taking up a lot of screen space.
\code
size_t c_strlncpy( char* dst, const char* src, size_t dst_size, size_t
n )
{
size_t src_length;
size_t copy_length;
c_return_value_if_fail( dst != NULL, 0 );
c_return_value_if_fail( src != NULL, 0 );
src_length = strlen( src );
if ( src_length > n ) {
src_length = n;
}
if ( dst_size == 0 ) {
return src_length;
}
copy_length = ( src_length >= dst_size ) ? dst_size - 1 :
src_length;
if ( copy_length ) {
memcpy( dst, src, copy_length );
}
dst[copy_length] = '\0';
c_unexpected( src_length >= dst_size );
return src_length;
}
\endcode
Then, if you call 'strlncpy' with a NULL 'src' or 'dst' argument,
you'll get a message saying where the developer goofed. The behavior
is defensive, the function returns a default value, and developer
errors are systematically reported. Then after the application is
developed, you can disable constraints if needed to increase
performance.
You can take it even further by splitting the different phases:
detection, report, and response, into their own function pointers (for
report and response) to provide even more complete customization of
the error handling of the program. You could change the report to add
a message to a log file, or popup a window, and the response could be
changed to 'assert' to abort on any invalid parameter, or to save the
program's current progress to a file, or contain a breakpoint to
inspect what's going on in a debugger when some interface constraint
is violated.
I use 'c_unexpected' to report cases where the semantics for the
function allow the condition, but it is typically a mistake. In the
'strlncpy' function above, it reports to the developer of a string
truncation, but takes no action.
This programming style validates that the constraints of a function's
parameter interface; it can inform or enforce that developers call the
function properly. Add unit testing to verify the behavior of the
function itself, to make sure that a function like 'c_strlncpy'
actually does what it's supposed to.
Best regards,
John D.