Przejdź do głównej zawartości

Metaprogramowanie cz2 – polimorfizm inaczej


Jak działa polimorfizm wie każdy programista języka C++ jest to przecież najpopularniejsza cech programowania obiektowego. Za pomocą funkcji wirtualnych, klas abstrakcyjnych jesteśmy w stanie tworzyć kod ogólny, bazując jedynie na interfejsach, zrzucając jednocześnie sposób rozwiązania na klasy pochodne dostarczające implementację. Wszystko pięknie ładnie i elastycznie. Niestety funkcje wirtualne są najzwyczajniej w świecie wolne. Jak wiadomo są miejsca w każdym dużym projekcie które wymagają minimalizacji opóźnień. Czy w tych miejscach należy zatem rezygnować z polimorfizmu i oferowanej przez niego elastyczności na rzecz maksymalizacji czasu działania kodu? Na szczęście są sposoby na obejście tego problemu, z pomocą przychodzą nam jak zwykle szablony.
Zacznijmy od zaprezentowania szablonu który w pozwala na zdefiniowanie typu na podstawie wartości (więcej szczegółów na ten temat można znaleźć w Nowoczesne projektowanie w C++. Uogólnione implementacje wzorców projektowych).

template<int V>
struct Int2Type
{
  enum { val = V };
};


A teraz rozważmy sytuacje w której mamy dwie lub więcej platform sprzętowych. Każda z nich ma realizować to samo zadanie, niestety ze względu na specyfikę platformy w każdej z nich sposób realizacji tego zadania jest zupełnie inny. W klasycznym podejściu obiektowym zdefiniowalibyśmy klasę interfejsową a następnie dziedziczące po niej klasy obsługujące konkretny sprzęt. Kod kliencki natomiast operowałby jedynie na klasie interfejsowej pod którą można podstawić dowolną klasą pochodną realizującą konkretne rozwiązania. Rozwiązanie to jest dobre elastyczne ale wprowadza opóźnienia ze względu na występujące funkcje wirtualne będące nieodzowną częścią klasy interfejsowej. Poniżej rozwiązanie tego samego problemu z wykorzystaniem szablonów.

//plik nagłówkowy
enum HardwareType {HardwareType_1 , HardwareType_2 };

template< HardwareType H>
struct HardwarePlatformDriver
{
public:
  //publiczne api
  void func()
  {
    func(Int2Type<H>() );
  }
private:
  //funkcje typowe dla danej platformy
  void func(Int2Type<HardwareType_1>)
  {
    //kod specyficzny dla danej platformy sprzętowej
  }
  
  void func(Int2Type<HardwareType_2>)
  {
    //kod specyficzny dla danej platformy sprzętowej
  }
};

//sposób użycia
typedef HardwarePlatformDriver< HardwareType_1 > Driver;
Driver driver;
driver.func(); //zostanie wywołana funkcja dla HardwareType_1

Jakie są inne zalety wykorzystania takiego rozwiązania. Dana konkretyzacja szablonu powoduje wygenerowanie kodu jedynie dla konkretnej platformy(kod dla funkcji odwołujących się do innych platform sprzętowych nie zostanie wygenerowany). W przypadku gdy w kodzie klienckim chcemy zmienić typ platformy modyfikujemy jednie nasz własny typ Driver i tyle. I w końcu najważniejsze nie mamy do czynienia z funkcjami wirtualnymi, nasz kod jest szybki(a przynajmniej szybszy od klasycznego rozwiązania).
Prezentowane rozwiązanie niesie za sobą pewne ograniczenia, mianowicie wymaga się aby w kodzie klienckim do definiowania typu używano aliasu na konkretną specjalizację naszego szablonu. Pozwoli to na łatwa zmianę stosowanej platformy gdy zajdzie taka potrzeba. I na końcu dodanie obsługi nowej wersji sprzętu wymaga modyfikacji zarówno samego szablonu(konieczność dodania odpowiednich metod) oraz rozszerzenia enuma HardwareType o nową wartość.

Komentarze

Popularne posty z tego bloga

Makra i preprocesor

Jako programista klasycznego C przyszło mi wielokrotnie ścierać się z makrami. Makro to zestaw instrukcji umieszczanych w kodzie są jednak interpretowane nie przez kompilator ale przez preprocesor. Preprocesor jest „pomocnikiem” kompilatora, zajmuje się on np. wstawianiem treści plików nagłówkowych do plików z kodem za pomocą instrukcji #include. Preprocesor należy rozumieć jako prymitywny edytor tekstu dokonujący „korekcji” plików z kodem źródłowym przed rozpoczęciem ich przetwarzania przez kompilator. Jakie są zalety wykorzystywania tych rozwiązań w kodzie? Tak naprawdę w języku C w czasach przed wprowadzeniem słowa kluczowego inline umożliwiały wstawianie kodu we wskazane miejsce. Należy bowiem pamiętać że każdorazowe wstawienie makroinstrukcji powoduje ingerencję w kod źródłowy (innymi słowy we wskazanym miejscu zostanie wstawiony stosowny fragment kodu). Łatwo obserwowalnym efektem częstego wykorzystywania makr w plikach z kodem jest rozrost pliku binarnego oraz jego szybsze d

C++11 Iterowanie

Zasadniczo iterowanie po elementach tablicy/vectora/listy itp. nie jest niczym nowym, interesującym ani pasjonującym, ot szara codzienność. Przyjrzyjmy się zatem jak robimy to najczęściej: class Image { public:   Image();   void rotate(float angle);   void display();   void* serialize(); }; class ShowImage { public:   void operator()(Image image)   {     image.display();   } }; //... std::vector<Image> imageCollection(10); for(int i=0; i<imageCollection.size(); ++i) {   imageCollection[i].display(); } for( std::vector<Image>::iterator                          currentImage = imageCollection.begin();                         currentImage < imageCollection.end();                         ++currentImage ) {   currentImage->display(); } std::for_each(imageCollection.begin(), imageCollection.end(),               ShowImage()); Dla naszych potrzeb stworzyliśmy sp

C++11 Variadic templates – szablony ze zmienną liczba parametrów

Witam w nowym roku. Dziś będzie o nowym elemencie szablonów czyli o szablonach ze zmienną liczbą parametrów. Ich implementacja i zachowanie różni się nieco od klasycznych szablonów. Nowa funkcjonalność pozwala na tworzenie bezpiecznych list typów. template<typename ...Ts> void variadic_template(){} Pierwszą nowością jest zastosowanie ...(wielokropka) przy określaniu typów szablonu. W ten sposób sygnalizujemy mnogość typów. Niestety nie mamy możliwości iterowania po kolejnych typach wewnątrz naszego szablonu. Ponadto taka definicja pozwala na stworzenie/wywołanie naszej szablonowej funkcji bez typów. variadic_template<>(); Aby tego uniknąć można uciec się do następującej sztuczki: template<typename T1, typename ...Ts> void variadic_template(T1 arg, Ts... args) W powyższym przykładzie jawnie wymuszamy podanie przynajmniej jednego typ dla naszej funkcji szablonowej. Jak już wspomniałem nie możemy jawnie iterować po typach przekazanych do