Przekazywanie różnych opcji jako jeden parametr (opt1 | opt2)

Kilka słów o przekazywaniu opcji typu: std::fstream.open(file, std::ios::out | std::ios::bin | std::ios::trunc).

Hej, w dzisiejszym wpisie chciałbym zająć się dość prostym zagadnieniem, które niekoniecznie jest oczywiste szczególnie dla osób zaczynających zabawę z programowaniem.

Wstęp


Mianowicie chodzi mi konstrukcje pokazane w poniższych kodach:

Pierwszy przykład powinien być powszechnie znany – ustawiamy w nim plik w trybie do zapisu (binarnie) wraz z usunięciem zawartości przy jego otworzeniu.

Drugi przykład pochodzi z SFML i tworzy okno z nagłówkiem (titlebar) oraz możliwością zmiany jego rozmiaru.

W obu przypadkach wszelkie opcje ustawiane są za pomocą 1 parametru przy użyciu magicznej pałki. Przyznam szczerze, że w czasach mojej nauki C++ ten mechanizm pozostawał dla mnie zagadką.

Dzisiaj chciałbym Wam przedstawić, że ten mechanizm jest banalnie prosty, zarówno do stworzenia jak i późniejszej obsługi.

Jak to działa


Aby użyć systemu tego typu należy pamiętać o jednej rzeczy: liczby zapisywane są binarnie. Co za tym idzie, to każdą zmienną możemy traktować jak kontener na flagi różnej długości (zależnie od typu), gdzie każdy element może przyjmować wartości 0 lub 1.

Sama zasada działania na kontenerach tego typu wymaga użycia operatorów logicznych, szczególnie przydatne okażą się:

  • | – or, suma logiczna;
  • & – and, iloczyn logiczny
  • << – shift-left, przesunięcie bitów w lewo

Warto zdać sobie sprawę jak one działają, poniżej przedstawiam krótkie wyjaśnienie.

OR (|)

AND (&)

SHL (<<)

Pierwsze podejście

Jak pewnie część z Was wie do sprawdzenia parzystości liczby nie musimy wykonywać operacji modulo, wystarczy że sprawdzimy wartość najmłodszego bitu liczby – można go traktować jako flagę nieparzystości:

Jak widzimy prostą sztuczką zaoszczędziliśmy całkiem sporo w stosunku do operacji modulo. Nic nie stoi na przeszkodzie aby pójść dalej i sprawić aby każdy z bitów liczby mógł mieć jakieś inne znaczenie, np. mówił czy plik jest w trybie do odczytu, zapisu, itd. Jak wspomniałem: liczbę można traktować jako tablicę wartości bool’owskich.

Poniżej przedstawiam prosty przykład pokazujący tworzenie „tablicy 8-elementowej” oraz zapalenie 3 flagi, a później jej odczytanie:

Jeżeli dodatkowo chcielibyśmy podnieść jeszcze 5 bit, to musimy zastosować or’a:

W ten właśnie sposób działają łączone opcje! Wystarczy, że każda z opcji przyjmie inne przesunięcie „1”, po czym zostanie połączona operatorem „|”. W następnym paragrafie pokazuję nieco większy przykład, który całą ideę demonstruje w bardziej uporządkowany sposób.

Duży przykład

Załóżmy, że piszemy bibliotekę oferującą obsługę na plikach. Przy otwarciu pliku możemy posłużyć się zestawem różnych opcji, wszystkie domyślnie są wyłączone. A są nimi:

  • tryb do odczytu;
  • tryb do zapisu (można otworzyć plik jednocześnie do zapisu i odczytu);
  • tryb binarny lub tekstowy;
  • otwarcie pliku z weryfikacją CRC.

Oprócz tego zakładamy, że mogą się pojawić jeszcze inne opcje i z różnych względów chcemy zostawić sobie 4 pola zarezerwowane.

Powyżej zdefiniowaliśmy sobie dla wygody wszystkie pola, w gruncie rzeczy można by wrzucić wszystkie wartości do enum, ale my trzymamy się konwencji C, więc skorzystamy z define‚ów ;)

Uwaga! Wiem, że enumy są w C, tutaj zastosowałem konwencję stosowaną np. do tworzenia quirków

W ostatnie linii dodatkowo zdefiniowaliśmy sobie flagę, która jest sumą dwóch wcześniejszych, zrobiliśmy to dla wygody użytkownika – założyliśmy że te opcje mogą pojawiać się razem dość często, więc czemu by nie pomóc użytkownikowi.

Zauważamy też, że mamy tylko 1 bit odpowiadający za tryb tekstowy lub binary, wynika to właśnie z tego że plik zawsze musimy uruchomić w jakimś trybie (jednym z tych dwóch) i może być tylko „1”. Bez sensu byłoby komplikować sobie życie używając kolejnego bitu, skoro tak nie musimy sprawdzać czy jakiś bit jest podniesiony, a nie czy np. obydwa.

Kolejnym krokiem jest stworzenie typedef‚a na uinta, oraz stworzenie prostego makra zwracającego 1 lub 0, w celu poinformowaniu nas o tym czy flaga jest podniesiona czy nie.

Powyżej widzimy wydrukowanie zawartości „rejestrów”, z racji że rejestry OPT nas mało interesują toje sobie darujemy.

Tutaj po prostu zerujemy wartość (dzięki @krzaq za zwrócenie uwagi odnośnie niemającego na nic wpływu xor’a).

W ostatnim listingu widzimy proste demo pokazujące sposób działania kodu, jak widzimy poniżej ustawione flagi są zgodne z tym co ustawiliśmy w kodzie.

Koniec


Mam nadzieję, że w dość jasny sposób przedstawiłem tę dość prostą ideę. W razie pytań, uwag, chęci wyrażenia swojej opinii to oczywiście zapraszam do systemu komentarzy.

Code ON!


  • Kukos

    Odnośnie makra:
    #define IS_SET(opt, reg) ((opt) & (reg) ? 1 : 0)

    Pamiętaj że opeartor cond ? true : false, asembleruje się tak samo jak if else, przez co mamy niepotrzebne porównanie, oczywiście najszybciej wykona się makro:

    #define GET_BIT(n, k) (((n) & (1ull <> (k))

    ale to spowoduje zmiana designu kodu, wiec jeśli chcemy tego uniknąć dajmy znać kompilatorowi w prosty sposób co chcemy zrobić:

    #define CAST_TO_BOOL(x) (!!(x))

    wtedy mamy:

    #define IS_SET(opt, reg) (CAST_TO_BOOL(opt & reg))

    dzięki temu zachowujemy cały czas pipeline, co przy newsu o nowych AMD-kach jest na „topie”