The Cast You Forgot
I spent forty minutes tracking down a bug that came down to a missing cast. The function returned gpointer. The caller assigned it to a gint. The compiler said nothing. The program did something creative with the pointer value instead of dereferencing it.
The Silent Conversion
C trusts you. That’s its most dangerous feature.
When you assign a void * to an int without a cast, GCC will often just… do it. Maybe a warning, maybe not, depending on your flags and the specifics. The value goes from being a memory address to being treated as a number. On a 64-bit system, you’ve also silently truncated it, since pointers are 8 bytes and int is typically 4.
The program doesn’t crash immediately. It just starts working with a number that happens to be the lower 32 bits of a heap address. If you’re lucky, this produces obviously wrong output. If you’re unlucky, it works fine on your test data and explodes in production.
Why Explicit Casts Matter
There’s a school of thought that says unnecessary casts are noise. “If the types are compatible, why clutter the code?” And for simple numeric widening — int to long, say — that’s a reasonable position.
But in a codebase with GObject, where half your types are boxed behind gpointer, explicit casts serve as documentation. They say “yes, I know this is a void *, and yes, I intend it to become a GDateTime *, and if that’s wrong, this cast is where you should start debugging.”
/* Bad: what is data? */
callback_func(data);
/* Better: I know what data is */
callback_func((MyStruct *)data);
The cast doesn’t change the generated code. The CPU doesn’t care. But the human reading this at 3 AM absolutely cares.
The GObject Pattern
GObject compounds this because its type system is dynamic. A GObject * might be anything in the inheritance hierarchy. The G_TYPE_CHECK_INSTANCE_CAST macro — hidden behind convenience macros like MY_OBJECT(obj) — adds runtime type checking in debug builds. But only if you use it.
Raw casts bypass all of that:
/* This checks the type at runtime (debug builds) */
MyObject *obj = MY_OBJECT(some_gobject);
/* This trusts you completely */
MyObject *obj = (MyObject *)some_gobject;
The first version will abort with a useful error message if some_gobject isn’t actually a MyObject. The second version will happily give you a pointer to whatever that memory contains, and you’ll find out it’s wrong three function calls later when a vtable lookup goes sideways.
The Debugging Story
The bug I was chasing looked like memory corruption. Values that should have been small integers were enormous. Structures that should have had valid fields were full of what looked like address fragments.
I added g_assert() calls. I ran under Valgrind. I stared at memory dumps.
The fix was adding (gint *) to one line and dereferencing it. The function had always returned the right data — it was just being interpreted as the data instead of a pointer to the data.
Forty minutes for one character. Well, nine characters: *(gint *). But who’s counting.
The Lesson
Every implicit conversion in C is a decision. You just didn’t realize you were making it. Explicit casts force you to be conscious of those decisions. They’re not noise — they’re assertions about your understanding of the data flow.
In a language with no runtime type safety, your casts are your tests. Write them like you mean them.