- term Static and Dynamic Dispatch
- We called the enum-based approach “traditional dispatch” because it was the familiar starting point, not because it is a standard pattern. It is actually called static dispatch, the compiler determines which code to execute at compile time based on the enum value. With function pointers, you are using dynamic dispatch, the decision of which function to call is made at runtime based on the function pointer stored in the object. Both achieve the same goal (executing type-specific behavior), but dynamic dispatch provides more flexibility at the cost of a small runtime overhead.
Refactoring with Function Pointers
New Requirements
Multiple receivers: Messages can now target multiple users/channels
- Example:
@alice @bob #general Hello everyone! - A single message goes to alice, bob, AND the general channel
Stop! Before You Continue…
Question: How would you implement these in your Ch02 code?
Think about it for a moment… The painful reality:
- You would need to rewrite
push_payload()parsing logic, completely. - The
payload_dataunion would need a new receiver list structure. - Multiple switch cases would change to handle arrays instead of single values.
- Processing logic would need loops for each receiver.
- Command parsing would need string splitting on semicolons.
In other words: you would need to touch almost every function, risking bugs and spending hours debugging edge cases! This is the moment to ask: “Is there a better way to organize my code?”
The answer is yes: function pointers.
The Current Problem
Look at your code from Ch02. Notice how every time you want to add a new payload type, you have to modify code in multiple places:
- Add a new enum value to
payload_kind - Add a new struct to the
payload_dataunion - Add a parsing case in
push_payload() - Add a processing case in
process_next() - Add a cleanup case (if you are properly freeing memory)
This is called tight coupling. Everything is intertwined, one change ripples through the entire codebase.
Imagine more future requirements, what if we need to add:
- Timestamps for each payload?
- Priority levels (urgent, normal, low)?
- Validation before processing?
- Logging when payloads are processed?
- JSON export of payloads?
- Filtering payloads by type?
With the current design, each feature requires changes in almost all functions. This does not scale! Real applications have dozens or hundreds of types, which requires continuous maintenance and extensions.
The Function Pointer Solution
Instead of using switch statements everywhere, we can store behavior with data, recall function pointers.
Core Idea: What if each payload could “know how to process itself”?
// Traditional approach: separate data and behavior
switch (payload->kind) {
case COMMAND_LOGIN:
printf("Command: login\n");
printf(" Arguments: [username: %s, password: %s]\n", ...);
break;
// ... more cases
}
// Function pointer approach: behavior stored with data
payload->process(payload); // Each payload knows how to process itself!
Your Mission
Refactor your Ch02 code to use function pointers instead of switch statements, and then you will requested to implement features in “New Requirements” section.
New Design Principle “Dynamic Dispatch”
- Remove the gigantic
payload_kindenum:- No more type enumeration!
- Each payload type is self-contained.
- Add a function pointer to each payload:
We use thestruct payload { void (*process)(const struct payload *self); // ... data }selfreference because the process function is a standalone function that exists outside of thestruct payloadinstance. We must explicitly pass the memory address of the struct so the function knows which specific object’s data it should operate on. - Each payload type sets its own processing function:
- Login command -> uses
process_login()function - Direct message -> uses
process_direct_message()function - … etc.
- Login command -> uses
- Simplify
process_next():void process_next(struct payload_buffer *buf) { struct payload *p = &buf->payloads[buf->process_base]; p->process(p); // That's it! No switch needed. buf->process_base++; }
Benefits You will See
- No more repetitive switch statements in processing code.
- Behavior and data are together: Each payload is self-contained.
- Extensibility: Adding new operations (like
validateorto_json) is straightforward. - Separation of Concerns: Adding new types is easier, as it does not require lots of changes in process orchestrator. Just implement business logic in a function and assign it.
What You will Learn
This exercise demonstrates the core principle of object-oriented programming:
“Objects bundle data and behavior together, allowing them to be treated uniformly.”
In C++/Java/Python, this is called polymorphism with virtual methods. In C, we achieve the same thing with function pointers.
Important Notes
- We are keeping this simple, just a few function pointers per payload.
- We are not building a full “virtual table” system yet. You will learn details of that in upcoming milestones.
- Focus on understanding how function pointers enable polymorphic behavior.
- Later exercises will show more sophisticated patterns.
Hints
- Keep your existing
payload_dataunion structure, simplify it by eliminating duplicate logic - Add a
void (*process)(struct payload *self)function pointer tostruct payload - Create separate
process_login(),process_direct_message(), etc. functions - In
push_payload(), after parsing, assign the appropriate function pointer - Your
process_next()becomes much simpler!
Expected Outcome
Your code should:
- Handle all the same payloads as Ch01.
- Have no switch statements in
process_next(). - Be easier to extend with new payload types.
- Demonstrate how function pointers enable polymorphism.
After completing this, you will understand why object-oriented languages use virtual methods. They solve the exact same problem you just solved with function pointers!
After completing the exercise on your own, inspect the notes on refactored and extended with new requirements solutions for this chapter.
We complained about switch/case being repetitive, but now that we have defined so many functions, is not this even more repetitive?
- Is it reasonable to store a pointer for each function inside the struct?
- No type-safety. What prevents conflicting behaviors (e.g., a command’s
processfunction but a message’sdestructor)? - We still had problems in the old approach…