glib has a nice set of macros you can use to enforce
invariants and preconditions in your code. GTK+ uses
these liberally---one of the reasons it's so stable and
easy to use. They all disappear when you define G_DISABLE_CHECKS or G_DISABLE_ASSERT, so there's no
performance penalty in production code. Using these
liberally is a very, very good idea. You'll find bugs
much faster if you do. You can even add assertions and
checks whenever you find a bug to be sure the bug
doesn't reappear in future versions---this complements
a regression suite. Checks are especially useful when
the code you're writing will be used as a black box by
other programmers; users will immediately know when and
how they've misused your code.
Of course you should be very careful to ensure your
code isn't subtly dependent on debug-only statements to
function correctly. Statements that will disappear in
production code should never
have side effects.
Figure 3
shows glib's precondition checks.
g_return_if_fail() prints a warning and
immediately returns from the current function if condition is FALSE.
g_return_val_if_fail() is similar but allows you
to return some retval.
These macros are incredibly useful---if you use them
liberally, especially in combination with GTK+'s
runtime type checking, you'll halve the time you spend
looking for bad pointers and type errors.
Using these functions is simple; here's an example from
the glib hash table implementation:
void
g_hash_table_foreach (GHashTable *hash_table,
GHFunc func,
gpointer user_data)
{
GHashNode *node;
gint i;
g_return_if_fail (hash_table != NULL);
g_return_if_fail (func != NULL);
for (i = 0; i < hash_table->size; i++)
for (node = hash_table->nodes[i]; node; node = node->next)
(* func) (node->key, node->value, user_data);
}
|
Without the checks, passing
NULL as a parameter to this function would
result in a mysterious segmentation fault. The person
using the library would have to figure out where the
error occurred with a debugger, and maybe even dig in
to the glib code to see what was wrong. With the
checks, they'll get a nice error message telling them
that NULL arguments are
not allowed.
glib also has more traditional assertion macros, shown
in Figure 4.
g_assert() is basically
identical to assert(), but
responds to
G_DISABLE_ASSERT and behaves consistently across
all platforms.
g_assert_not_reached() is also provided; this is
an assertion which always fails. Assertions call abort() to exit the program and
(if your environment supports it) dump a core file for
debugging purposes.
Fatal assertions should be used to check internal consistency of a function or
library, while
g_return_if_fail() is intended to ensure sane
values are passed to the public interfaces of a program
module. That is, if an assertion fails, you typically
look for a bug in the module containing the assertion;
if a g_return_if_fail() check
fails, you typically look for the bug in the code which
invokes the module.
This code from glib's calendrical calculations module
shows the difference:
GDate*
g_date_new_dmy (GDateDay day, GDateMonth m, GDateYear y)
{
GDate *d;
g_return_val_if_fail (g_date_valid_dmy (day, m, y), NULL);
d = g_new (GDate, 1);
d->julian = FALSE;
d->dmy = TRUE;
d->month = m;
d->day = day;
d->year = y;
g_assert (g_date_valid (d));
return d;
}
|
The precondition check at the beginning ensures the
user passes in reasonable values for the day, month and
year; the assertion at the end ensures that glib
constructed a sane object, given sane values.
g_assert_not_reached() should
be used to mark "impossible" situations; a common use
is to detect switch statements that don't handle all
possible values of an enumeration:
switch (val)
{
case FOO_ONE:
break;
case FOO_TWO:
break;
default:
/* Invalid enumeration value */
g_assert_not_reached();
break;
}
|
All of the debugging macros print a warning using
glib's g_log() facility,
which means the warning includes the name of the
originating application or library, and you can
optionally install a replacement warning-printing
routine. For example, you might send all warnings to a
dialog box or log file instead of printing them on the
console.