Hello! Welcome back.
In our last lesson, we explored the power of callbacks, where you pass a function to another function to control its behavior. We saw how this creates flexible and decoupled code, from a generic sorting algorithm to the UDS request handling in an AUTOSAR stack.
Today, we'll take that concept a step further. Instead of passing a single function pointer, we'll organize a collection of them into an array. This creates a dispatch table (or jump table), a highly efficient and maintainable design pattern for handling a set of commands or events. You'll learn how to replace cumbersome if-else or switch statements with this elegant solution, a technique prized in high-performance embedded systems for its predictability and scalability.
By the end of this lesson, you will be able to design a dispatch table using an array of function pointers to replace if-else or switch chains for command processing.
The Problem: The Overgrown switch Statement
Imagine you're writing firmware for a gateway ECU that routes commands. A common approach is to use a switch statement based on a command ID.
// Command IDs
#define CMD_READ_SENSOR_A 0
#define CMD_WRITE_CONFIG_B 1
#define CMD_RUN_DIAGNOSTIC 2
#define CMD_RESET_DEVICE 3
// ... potentially dozens more
void process_command(int command_id, char* data) {
switch (command_id) {
case CMD_READ_SENSOR_A:
handle_read_sensor_a(data);
break;
case CMD_WRITE_CONFIG_B:
handle_write_config_b(data);
break;
case CMD_RUN_DIAGNOSTIC:
handle_run_diagnostic(data);
break;
case CMD_RESET_DEVICE:
handle_reset_device(data);
break;
// ... imagine this list grows to 50 commands
default:
handle_unknown_command();
break;
}
}
While this works, it has several drawbacks that become painful in large projects:
- Maintainability: As the number of commands grows, the
switchblock becomes massive and difficult to read and manage. - Error-Prone: It's notoriously easy to forget a
breakstatement, leading to unintended fall-through bugs that can be difficult to track down. - Performance: While a compiler can optimize a dense
switchstatement (with contiguouscasevalues) into an efficient jump table, it's not guaranteed. If thecasevalues are sparse (e.g., 0, 1, 10, 100), the compiler will likely generate a less efficient sequence of comparisons (anif-else-ifchain).
The Solution: A Dispatch Table
A dispatch table solves these problems by using an array of function pointers. The command ID is used as a direct index into the array to find the correct handler function.
Here is a simple illustration of the concept.

Now, let's dive into a more formal analysis of why this pattern is so powerful in a professional context.
How to Create Jump Tables via Function Pointer Arrays in C and C++
The article "How to Create Jump Tables via Function Pointer Arrays in C and C++" from Barr Group provides a superb analysis of this topic from an embedded systems perspective. It directly addresses the common arguments against using function pointers and demonstrates their superiority over switch statements in many contexts.
Please read the sections under the headings "Are function pointers dangerous?" and "Leave it to the optimizer?". These sections directly tackle the trade-offs between using a dispatch table and relying on a switch statement, which is a key discussion point in a technical interview.
As the article highlights, the perceived "danger" is easily mitigated with bounds checking, and explicitly coding the jump table gives you predictable performance, a critical requirement in real-time and safety-critical systems like those you work on.
Designing a Robust Command Processor
Let's refactor our earlier switch-based command processor into a robust dispatch table.
Step 1: Define the Function Signature with typedef
All our handler functions should have the same signature. We'll use typedef (as we practiced two lessons ago) to create a clean alias for our function pointer type.
#include <stdio.h>
#include <stdint.h>
// All command handlers will follow this signature.
// They take a data payload and return a status code.
typedef int (*CommandHandler)(char* data);
// Function prototypes for our handlers
int handle_read_sensor_a(char* data);
int handle_write_config_b(char* data);
int handle_run_diagnostic(char* data);
int handle_reset_device(char* data);
int handle_unknown_command(char* data);
Step 2: Create the Dispatch Table
The table is an array of our CommandHandler type. Declaring it static const is best practice:
static: It keeps the table private to this file.const: It ensures the table is placed in read-only memory (ROM/Flash) and cannot be accidentally modified at runtime, a crucial point for MISRA compliance and functional safety (ISO 26262).
// Command IDs as indices
#define CMD_READ_SENSOR_A 0
#define CMD_WRITE_CONFIG_B 1
#define CMD_RUN_DIAGNOSTIC 2
#define CMD_RESET_DEVICE 3
#define NUM_COMMANDS 4
static const CommandHandler command_table[NUM_COMMANDS] = {
[CMD_READ_SENSOR_A] = handle_read_sensor_a,
[CMD_WRITE_CONFIG_B] = handle_write_config_b,
[CMD_RUN_DIAGNOSTIC] = handle_run_diagnostic,
[CMD_RESET_DEVICE] = handle_reset_device
// Designated initializers (a C99 feature) make this very readable!
};
Step 3: The Processing Logic
The new process_command function is now much simpler and more efficient.
int process_command(uint8_t command_id, char* data) {
// 1. Bounds checking: The most critical safety step.
if (command_id < NUM_COMMANDS) {
// 2. Look up and call the function.
// The core of the dispatch mechanism!
return command_table[command_id](data);
} else {
// 3. Handle invalid commands gracefully.
return handle_unknown_command(data);
}
}
This design is clean, fast, and scalable. To add a new command, you simply:
- Define the new handler function.
- Add a new command ID and increment
NUM_COMMANDS. - Add the new function to the
command_tablearray.
The coreprocess_commandlogic never changes.
Applications in Your Domain: Beyond Simple Commands
The dispatch table pattern is fundamental in embedded systems. Your experience with AUTOSAR and microcontrollers has already exposed you to it, perhaps without you thinking of it in these terms.
How to Create Jump Tables via Function Pointer Arrays in C and C++
Let's revisit the Barr Group article to see some concrete examples from the embedded world. These will resonate with your work in automotive software.
Please read the section "Arrays of function pointers", focusing on the subsections "Communication protocols", "Timed task list", and "Interrupt vector tables". These are classic applications of the dispatch table pattern.
Let's connect those examples directly to your background:
- Interrupt Vector Table (IVT): This is the quintessential dispatch table, implemented in hardware. The CPU receives an interrupt number (the "command ID") and uses it as an index into the IVT in memory to find the address of the corresponding Interrupt Service Routine (ISR).
- Timed Task List (Simple Scheduler): The example in the article shows a non-preemptive, cooperative scheduler. This is a common pattern in smaller ECUs that don't need a full RTOS. The "table" here is an array of structs, each containing timing information and a function pointer to a task. This is exactly how many basic software modules in AUTOSAR are scheduled within the
mainfunction loop, being called periodically based on OS ticks. - Communication Protocols: The
process_read()example from the article is very clever. It's a perfect model for parsing text-based diagnostic commands or even SOME/IP method calls where a method ID needs to be mapped to a handler function.
Conclusion
You've now seen how to design and use a dispatch table to create clean, efficient, and scalable command-handling logic. This pattern is a significant step up from basic switch statements and is a hallmark of professional C programming, especially in performance-critical domains like automotive software.
Key Takeaways for Your Interview:
- Explain the
switchstatement's limitations: Discuss its lack of predictable performance (especially with sparse cases) and its poor maintainability and proneness tobreakerrors in large-scale projects. - Describe the Dispatch Table Solution: Explain it as an array of function pointers where the command ID serves as a direct index. Emphasize the O(1) performance characteristic.
- Detail a Robust Design: Mention using
static constfor the table to ensure it's in ROM and is thread-safe for reading. Crucially, emphasize the need for bounds checking before indexing the array. - Provide Concrete Examples: Use the simple calculator or a command parser as a code example. Then, elevate your answer by connecting the pattern to real-world embedded concepts like an Interrupt Vector Table, a simple task scheduler, or the internal workings of an AUTOSAR module like the DCM. This demonstrates deep architectural understanding beyond just C syntax.
In our next and final lesson on function pointers, we will synthesize what we've learned about callbacks and dispatch tables to analyze their architectural use in an AUTOSAR context, focusing on notification patterns.