5. Typy złożone

5.4. Unie

Unie z punktu widzenia kompilatora są bardzo podobne do struktur. Główna różnica polega na tym, że o ile w przypadku struktur każde kolejne pole jest alokowane w innym miejscu pamięci, o tyle w przypadku unii wszystkie pola są alokowane w tym samym obszarze pamięci, i współdzielą swoją wewnętrzną tożsamość. Oczywiście - powoduje to, iż w danym czasie w unii można przechowywać tylko jeden typ wartości (tylko jedno pole). 

C++ nie sprawdza, co w danym obszarze jest zapisane - zostawiono to programiście. Natomiast sam fakt że zostawiono - nie zwalnia Was z tego. Nie powinno się odczytywać z unii typu, który nie został tam zapisany. Unie powstały w celu współużywania tej samej pamięci - stosujemy je gdy wiemy, że w danym momencie możliwe jest istnienie tylko jednego typu obiektu z podanego zbioru, a nie po to, by dostać się do pamięci jednego obiektu poprzez interfejs innego obiektu (tzw type-punning). To jest różnica między C++ a C. Stare C pozwalało na odczyt nieaktywnego pola unii (czyli nie tego, które ostatnio zostało zapisane). W C++ jest to zachowanie niezdefiniowane.


  union unia
  {
    int calk;
    char znak[];
  };
  unia u1;
  u1.znak[0] = 'x'; // teraz w unii jest tablica znaków
  u1.calk = 10 + 0x0f00; // teraz jest liczba

Stworzyliśmy w ten sposób jednolity obszar pamięci, który możemy interpretować (w zależności od odwołania) jako liczbę typu int, albo jako tablicę znaków (bajtów).

Natomiast nie powinniście odczytywać wartości liczby dzielonej modulo przez 255 korzystając z unii - nie w C++, mimo że w C byłoby to poprawne. Poniższy kod - choć w większości przypadków zadziała - nie powinien być wykorzystywany.


  u1.calk = 10 + 0x0f00; // teraz jest liczba
  std::cout << u1.znak[0]; 

Jeśli zdecydujecie się na wykorzystanie unii, pamiętajcie, że:

  • Unie nie mogą mieć konstruktorów, destruktorów oraz pól statycznych,
  • W tym samym obszarze pamięci mogą być przechowywane różne zmienne,
  • Unia zajmuje tyle pamięci co największe z jej pól.

Unie są wyjątkowo mało przenośnie, w zasadzie ich zastosowanie ogranicza się do kilku przypadków: przechowywania zmiennych wariantowych (choć tu mamy znacznie lepsze rozwiązania, np std::variant), lub w kodzie niskopoziomowym. Unikajcie więc stosowanie nieoznakowanych unii, a jak potrzebujecie oszczędzać pamięć, a nie ufacie typowi wariant, to zbudujcie klasę opakowującą "oznakowaną" unię - oznakowaną czyli z polem typu:


  union Unia
  {
    int calk;
    char znak[];
  };
  enum class Typ { Calkowita, Znakowa };
  struct Oznakowana {
  	Unia u1; 
    Typ typ; 
  };
  

Dodatkowo, możliwe jest stosowanie unii która ani nie ma zdefiniowanej nazwy, ani nie jest zdefiniowana żadna zmienna jej typu. Oba poniższe przykłady kodu są prawidłowe:


struct
{
  int a;
  union {
    long l
    char c[4];
  };
} dzial;

union
{
        struct { char c1, c2; short s; } p;
        long l;
};

Pusta definicja unii nie oznacza tworzenia jakiegoś nowego typu, jest jedynie informacją, że dane pola współdzielą wspólny obszar pamięci.