Embedded C ( or C, C++) programming allows building complex constructs out of simple ones to establish desired functionality.
While such designs get the job done and are widely used, managing such designs in a large project over a longer period of time is tedious. They are also more prone to error when design updates are performed. When building Safety critical applications it is essential to avoid such complex constructs to keep the software less error prone and more safe.
Nested Switches
Nested switches (Switch within a Switch or If-Else within a Switch) are a commonly used complex construct when design State machines. While the implementation effort is less, the construct is not recommended for Safety (MISRA-C).
In most cases it is also not possible to eliminate such design. But a different implementation strategy could be used to avoid Nested Switches, in a smart way. Often they also end up increasing your code's efficiency (Less code size and more execution speed).
Let's see an alternate implementation for Nested-Switches using a generic "menu selection" example.
Consider a user menu based application, where it is required to perform a function based on the user selected values. Assume the user is required to select a menu option and a sub-menu option stored under "mState" and "smState" respectively.
typedef enum {
MenuState1 = 0,
MenuState2,
MenuState3,
} MenuStates;
typedef enum {
MenuSubState1 = 0,
MenuSubState2,
MenuSubState3,
MenuSubState4,
} SubMenuStates;
MenuStates mState;
SubMenuStates smState;
The application shall execute task functions based on the selected main-menu and sub-menu states as given below
MenuStates Value | SubMenuStates Value | Function |
MenuState1 | MenuSubState1 | Task11 ( ) |
MenuState1 | MenuSubState2 | Task12 ( ) |
MenuState1 | MenuSubState3 | Task13 ( ) |
MenuState1 | MenuSubState4 | Task14 ( ) |
| | |
MenuState2 | MenuSubState1 | Task21 ( ) |
MenuState2 | MenuSubState2 | No Action |
MenuState2 | MenuSubState3 | Task23 ( ) |
MenuState2 | MenuSubState4 | Task22 ( ) |
| | |
MenuState3 | MenuSubState1 | Task31 ( ) |
MenuState3 | MenuSubState2 | Task32 ( ) |
MenuState3 | MenuSubState3 | Task33 ( ) |
MenuState3 | MenuSubState4 | No Action |
The above scenario shall be designed using Nested-Switch blocks as below,
switch (mState)
{
case MenuState1 :
switch (smState)
{
case MenuSubState1:
Task11();
break;
case MenuSubState2:
Task12();
break;
case MenuSubState3:
Task13();
break;
case MenuSubState4:
Task14();
break;
default :
break;
}
break;
case MenuState2 :
switch (smState)
{
case MenuSubState1:
Task21();
break;
case MenuSubState3:
Task23();
break;
case MenuSubState4:
Task24();
break;
default :
break;
}
break;
case MenuState3 :
switch (smState)
{
case MenuSubState1:
Task31();
break;
case MenuSubState2:
Task32();
break;
case MenuSubState3:
Task33();
break;
default :
break;
}
break;
default :
break;
}
In the above code, the respective task is performed based on the values of mState (Main-menu option) and smState (sub-menu option).
It is evident from the above code that a nested-switch implementation is complex. And the code size only increases with more states. Maintaining such a code is tedious and would require complete re-implementation at times.
Therefore the above code should be replaced with an alternative implementation for simplicity and scaling. A better and safer approach for the above scenario is to use a Multi-dimension Array based implementation.
Multi-dimensional Array
The Nested switch implementation shall be replaced with a multi-dimension array implementation with following considerations,
Create a multi-dimension array (dimension = Nesting level + 1). In this case the nesting level is 1. Hence a 2D array.
Considering the 2 state variables (mState and smState) as indexes, fill the Function pointers as array values.
Modify the enum declarations to add an additional value to indicate the size.
For cases where no action is to be done, fill with NULL.
typedef enum {
MenuState1 = 0,
MenuState2,
MenuState3,
MenuStateSize
} MenuStates;
typedef enum {
MenuSubState1 = 0,
MenuSubState2,
MenuSubState3,
MenuSubState4,
MenuSubStateSize
} SubMenuStates;
MenuStates mState;
SubMenuStates smState;
void (* const TaskTable[MenuStateSize][MenuSubStateSize])(void) = {
{TASK11, TASK12, TASK13, TASK14},
{TASK21, NULL, TASK23, TASK24},
{TASK31, TASK32, TASK33, NULL }
};
if(TaskTable[mState][smState] != NULL) //Check for NULL
{
(*TaskTable[mState][smState])(); /* Calls the appropriate Task function based on the main-menu and sub-menu states. */
}
Here, we have fully replaced the nested switches, with the use of a 2D array with Function pointer. This considerably reduces the Cyclomatic complexity, allows for easy modification and scalable.
It is essential to check for NULL and also the array indexes are within range to avoid issues.
There could also be situations where usage of function pointers is not desired. In such cases, the developer shall take a call to use the best method.
Comments