sobota, 9 kwietnia 2011

Compound literals, czyli jak wygodnie przekazywać parametry złożone

Autor: tmf
Redakcja: Dondu

Kurs języka C: Spis treści

Jakiś czas temu na Elektrodzie pojawiło się pytanie o to, w jaki sposób wygodnie przekazywać do funkcji złożony argument.

O co chodzi?
Spójrzmy na prototyp poniższej funkcji:
void foo(int *ptr);
Nasza funkcja foo przyjmuje jako argument wskaźnik na int. Skoro musimy jej przekazać wskaźnik, to znaczy, że jej wywołanie musi wyglądać tak jak poniżej:
int var=10;
foo(&var);
Wszystko pięknie, ale w powyższym trywialnym przykładzie, aby funkcji przekazać wartość 10, musimy utworzyć tymczasową zmienną var, nadać jej wartość, a następnie przekazać adres zmiennej var do funkcji foo. Z oczywistych powodów konstrukcje typu:
foo(&10);
lub też:
foo(10);
nie zadziałają wcale (błędy podczas kompilacji) lub nie zrobią tego, czego oczekujemy. Jak ten problem rozwiązać? Standard C99 wprowadza nam eleganckie rozwiązanie powyższego problemu pod postacią tzw. literałów złożonych (compound literals). Na czym one polegają? Ogólnie zamiast stworzyć tymczasowe zmienne poza wywołaniem funkcji, tak jak to pokazałem w powyższym przykładzie, możemy utworzyć tymczasowe „obiekty” w samym wywołaniu funkcji:
foo(&(int){10});

Jak widzimy zastosowanie takiego literału polega na podaniu w nawiasie okrągłym jego typu, a następnie w nawiasie klamrowym inicjalizatora typu – czyli po prostu wartości tworzonego obiektu.

Pamiętaj, że o ile zapis z wykorzystaniem literału złożonego jest krótszy i bardziej elegancki, to współczesne kompilatory są na tyle sprawne, że zarówno zapis z literałem złożonym, jak i zapis wykorzystujący zmienne tymczasowe powinien wygenerować identyczny kod asemblerowy. Stąd też stosowanie literałów złożonych w dużej mierze związane jest tylko z większą czytelnością zapisu kodu.

Obiekt będący literałem złożonym istnieje tylko w czasie wywołania funkcji foo – po jej zakończeniu jest automatycznie niszczony, nie możemy się już do niego odwoływać. Korzyści ze stosowania tego typu literałów jeszcze wyraźniej widać w przypadku struktur danych. Rzućmy okiem na kolejny przykład:

typedef union
{
   struct
   {
      uint8_t IB02      : 3;
      uint8_t AM        : 1;
      uint8_t ID        : 2;
      uint8_t TY        : 2;
      uint8_t DMode     : 1;
      uint8_t NoSync    : 1;
      uint8_t WMode     : 1;
      uint8_t DenMode   : 1;
      uint8_t IB12      : 1;
      uint8_t DFM       : 2;
      uint8_t VSMode    : 1;
   };
   uint16_t word;
} ssd2119_EntryMode_Reg;

Zdefiniowaliśmy sobie unię struktury anonimowej i typu uint16_t, który będzie reprezentował 16-bitową wartość jej pól. Dzięki temu mamy wygodny dostęp do poszczególnych pól (jak łatwo zauważyć pola te definiują bity sterujące jednego z rejestrów kontrolera SSD2119), a pole word będzie zawierać bieżącą reprezentację binarną całej struktury. Zadeklarujmy sobie teraz prototyp funkcji wykorzystującej powyższą unię:
void ssd2119_SendCmdWithData(uint8_t cmd, uint16_t data);
Jak widzimy, nasza unia przekazywana jest do funkcji jako argument data o typie uint16_t, a nie ssd2119_EntryMode_Reg. Dlaczego tak? Nasza funkcja ssd2119_SendCmdWithData wysyła do kontrolera LCD po prostu polecenie przekazywane jako argument cmd, a pole data zawiera tylko dane, które razem z poleceniem wysyłamy. Dane te są zależne od wybranego polecenia, w związku z tym ich typ może być inny niż ssd2119_EntryMode_Reg. Mamy już strukturę danych, a także funkcję wysyłającą polecenie do kontrolera, połączmy to teraz w całość. Klasycznie, polecenie zapisu do rejestru Entry Mode kontrolera wysłalibyśmy tworząc zmienną tymczasową:
ssd2119_EntryMode_Reg tmp={.DFM=0b11, .DenMode=1, .TY=0b01, .ID=0b11, .AM=0};
ssd2119_SendCmdWithData(0x11, tmp.word);
Ale możemy też to uczynić z wykorzystaniem literałów złożonych:
ssd2119_SendCmdWithData(0x11, (ssd2119_EntryMode_Reg){.DFM=0b11, .DenMode=1, .TY=0b01, .ID=0b11, .AM=0}.word);
W tym przypadku utworzony został obiekt tymczasowy o typie ssd2119_EntryMode_Reg, który inicjalizujemy analogicznie jak to robimy w przypadku inicjalizacji struktur/unii, a do funkcji, która oczekuje przecież typu uint16_t, przekazujemy wartość pola word reprezentującego naszą unię. Proste, prawda? W ten sposób uzyskujemy o wiele krótszy zapis i moim zdaniem bardziej przejrzysty.

Wykorzystanie takich literałów daje także inne korzyści, m.in. możliwość łatwej inicjalizacji typów złożonych, np. list – co wykorzystujemy np. podczas tworzenia menu i obiektów związanych z grafiką. Liczne przykłady wykorzystujące literały złożone wykorzystuję w swoich książkach „AVR. Praktyczne projekty” i „AVR. Układy peryferyjne”. Można się z nimi zapoznać pobierając darmowe przykłady (ze strony wydawnictwa Helion). Warto to zrobić, bo jak widać, literały złożone to prosty sposób na tworzenie przejrzystych programów. Chodzi tu nie tylko o uzyskaną zwartość kodu, ale także o niezaśmiecanie przestrzeni nazw zmiennymi tymczasowymi, co pozwala nam uniknąć wielu problemów.

Kurs języka C: Spis treści

1 komentarz:

  1. Rewelacyjny przykład z wykorzystaniem unii. Niby artykuł o literałach, a dla mnie odkrywcze stało się praktyczne zastosowanie unii :D Hehehe.. dzięki :)

    OdpowiedzUsuń