To już ostatni wpis z serii dotyczącej zastosowań tablic w C. Dzisiejszym tematem będą tablice wskaźników na funkcje. Pozwalają one w jednolity sposób obsługiwać różne zachowania programu, czyli są rodzajem polimorfizmu. Czasem bywają niezwykle przydatne.
Przypisywanie komend przyciskom pilota
Dawno temu w ramach pracy inżynierskiej robiłem pojazd sterowany dzięki pilota do telewizora. Jednym z ciekawszych problemów w sofcie było przypisywanie komend do przycisków pilota. Chodziło o to, aby dostępne funkcje dało się przypisać do każdego przycisku pilota. Konfiguracja mogła się zmieniać podczas działania programu. To był mój pierwszy program, w którym na większą skalę użyłem wskaźników na funkcje.
W jaki sposób coś takiego można zaimplementować? Potrzebujemy listy dostępnych identyfikatorów komend i tablicę funkcji obsługujące te komendy.
enum rc_command { RC_COM_POWER, RC_COM_FORWARD, RC_COM_BACK, RC_COM_UP, RC_COM_DOWN, RC_COM_STOP, RC_COM_SPEED_UP, RC_COM_SPEED_DOWN, ... RC_COM_EMPTY, RC_COM_CNT, } typedef void (*command_fun_t)(void); static const command_fun_t rc_command_fun_table[RC_COM_CNT] = { supply_shutdown, motor_forward, motor_back, motor_up, motor_down, motor_stop, ... NULL, };Potrzebujemy również identyfikatorów przycisków na pilocie i mapowania pomiędzy id przycisku i id komendy.
enum rc_button { RC_BUTTON_POWER, RC_BUTTON_1, RC_BUTTON_2, ... RC_BUTTON_VOLUME_UP, RC_BUTTON_VOLUME_DOWN, ... RC_BUTTON_CNT, }; static enum rc_command buttons_to_commands_mapping[RC_BUTTON_CNT] = { RC_COM_POWER, RC_COM_EMPTY, RC_COM_EMPTY, ... RC_SPEED_UP, RC_SPEED_DOWN, ... };Mając te tablice możemy użyć poniższych funkcji do wykonywania komend i przypisywania ich poszczególnym przyciskom.
void run_cmd_for_button(enum rc_button button_id) { //todo: check if id in range and if fun ptr not null rc_command_fun_table[buttons_to_commands_mapping[button_id]](); } void bind_cmd_with_button(enum rc_button button_id, enum rc_command command_id) { buttons_to_commands_mapping[button_id] = command_id; }Omówienie implementacji
W oryginalnym sofcie swojej inżynierki bezpośrednio wpisywałem wskaźniki na funkcję do tablicy z mapowaniem. Jednak dużo bezpieczniej jest zapisywać wskaźniki na funkcje do tablic typu const w czasie kompilacji, a w runtime korzystać z identyfikatorów. Dzięki temu łatwiej uchronić się przed skokiem w niechciane miejsce jeżeli w tablicy znajdą się błędne dane.
Jedną z możliwych komend jest RC_COM_EMPTY, czyli komenda pusta. Bywa ona bardzo przydatna – w końcu zwykle mamy więcej przycisków na pilocie niż możliwych funkcji. Taką komendę musimy oczywiście specjalnie obsługiwać w kodzie po prostu ignorując funkcję. Ewentualnie możemy również przypisać jej funkcję, która nic nie robi.
Przy okazji mamy też możliwość przypisywania tej samej komendy różnym przyciskom, a choćby tej samej funkcji różnym komendom. Czasami taka elastyczność jest bardzo przydatna.
Zastosowania
Tablica wskaźników na funkcje przydaje się, kiedy potrzebujemy skonfigurować różne zachowania obsługiwane w ten sam sposób. Możemy tak obsługiwać nie tylko przyciski, ale także np. menu na wyświetlaczach, czy protokoły komunikacji, czy obsługi błędów. Jest to rodzaj polimorfizmu. Mamy zawsze taką samą prostą obsługę i nie obchodzi nas jaka konkretnie funkcja jest wykonywana.
Czasami z jednym elementem chcemy powiązać więcej zachowań. Wtedy zamiast pojedynczej funkcji możemy w tablicy przechowywać cały interfejs. Jak tworzyć interfejsy w C opisywałem już kiedyś na blogu. w uproszczeniu – tworzymy strukturę zawierającą kilka wskaźników do funkcji.
Podsumowanie
Tablice wskaźników na funkcje, podobnie jak techniki opisywane w poprzednich częściach, są nieraz lepszą alternatywą dla wielkich instrukcji switch-case, czy if-else. Tablice pozwalają na szybsze działanie programu i łatwiejszą implementację.
Implementacja oparta na tablicach, a szczególnie o ile zawiera jeszcze wskaźniki na funkcje, może być trudna do zrozumienia i debugowania. Szczególnie o ile nie jesteśmy do niej przyzwyczajeni. Dlatego o ile użyjemy tablic w prostych sytuacjach spowodują one tylko zaciemnienie kodu. Najlepiej zawsze zaczynać od najprostszej implementacji i przechodzić na bardziej skomplikowaną – jak omawiane tutaj tablice – dopiero kiedy faktycznie tego potrzebujemy. Nie zaszkodzi również udokumentować, dlaczego używamy tablicy i jak się z nią obchodzić.