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
Prześlij komentarz