Tablice wskaźników na funkcje

Zastosowania tablic w C - wszystkie wpisy

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 za pomocą 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śli 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 nawet 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 skrócie – 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 jeżeli zawiera jeszcze wskaźniki na funkcje, może być trudna do zrozumienia i debugowania. Szczególnie jeżeli nie jesteśmy do niej przyzwyczajeni. Dlatego jeżeli 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ć.

Zastosowania tablic w C - Nawigacja

4 Comments

  1. To ja dodam od siebie, że żeby zabezpieczyć się przed większymi modyfikacjami pozycji w takich tablicach można tak użyć enuma w tablicy: https://gist.github.com/dambo1993/9dae35ec474ebc215eeea9f52b2f7659

    • GAndaLF

      17 września 2019 at 23:14

      Przypisywanie do pól tablicy po indeksach jest chyba od c99 i nie we wszystkich projektach można używać. Także czasem trzeba uważać, ale jak możesz, to spoko.

  2. Dzięki, użyłem przedstawionej w tym wpisie techniki do podmianki handlerów (wskaźnik na strukturę posiadającą 3 wskaźniki na funkcje) dla przycisków.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *