Klasy parametryzowane wytycznymi

Klasy parametryzowane wytycznymi spopularyzował i omówił Andrei Alexandrescu[1]. Wytyczne są techniką metaprogramowania w C++ z wykorzystaniem szablonów, umożliwiającą uzyskanie dużo większej elastyczności w projektowaniu klas.

Konstruowanie klas parametryzowanych wytycznymi polega na składaniu klasy z wykorzystaniem innych klas (tzw. wytycznych), z których każda określa pewien fragment zachowania głównej klasy. W praktyce, można myśleć o klasach wytycznych jako o pewnym rodzaju wtyczek, które doczepiane do klas, umożliwiają elastyczne, bezpieczne typologicznie i efektywne dostosowywanie klas przez użytkownika, bez ręcznego pisania setek różnych wariantów, które różnią się tylko szczegółami działania. Jest to technika bardzo podobna do wzorca projektowego Strategia, tyle tylko że tam zmiana działania obiektów może następować dynamiczne, podczas działania programu, natomiast wytyczne ustalają zachowanie już podczas kompilacji.

Przykład

edytuj

Załóżmy, że piszemy klasę inteligentnego wskaźnika i, ze względów efektywnościowych, chcemy dać użytkownikowi możliwość określenia czy w danym przypadku ma zachodzić przeprowadzane testowanie poprawności, czy nie. Jednym z możliwych rozwiązań tego problemu jak dodanie parametru do konstruktora obiektu, który determinuje jak inteligentny wskaźnik ma się zachowywać. Jednak wspomniane względy efektywnościowe przestrzegają nas przed tym, gdyż należałoby przeprowadzać testowanie if w każdym miejscu kodu, co wprowadzałoby dodatkowy narzut nawet wtedy, gdy użytkownik wybrałby wariant bez testowania poprawności.

Standardowa klasa SmartPtr może zostać skrótowo zapisana w ten sposób:

 template <class T>
 class SmartPtr
  {
   // ...
  };

Aby wykorzystać wytyczne, do klasy SmartPtr musimy dodać jeszcze jeden parametr szablonu, który będzie ustalał sposób obsługi błędów. Tak więc, od tej pory nasza klasa SmartPtr może wyglądać np. tak:

 template <class T, class ErrorHandling>
 class SmartPtr : public ErrorHandling
  {
   // ...
  };

Dziedziczymy publicznie od klasy ErrorHandling, tak więc wszystkie metody tej klasy od tej pory wchodzą w skład klasy SmartPtr. Co więcej, zachowanie tych metod zmienia się w zależności od tego jaką klasę wyślemy w miejsce ErrorHandling. Tak więc, gdy wywołamy np. metodę void ErrorHandling :: Check(), to gdy ErrorHandling będzie równe NoChecking, wtedy nie będzie żadnego sprawdzania. Natomiast gdy wyślemy StrictCheckcing, to każda dereferencja wskaźnika przy wywołaniu (w tym przypadku) metody Get() będzie sprawdzana:

template <class T>
 struct NoChecking
  {
   void Check(T * _ptr) { /* pusto, brak sprawdzania poprawności */ }
  }

 template <class T>
 struct StrictChecking
  {
   void Check(T * _ptr) { if (0 == _ptr) { /* obsłuż błąd */ } }
  }

 // Opis klasy:
 //  Wytyczna ErrorHandling musi posiadać metodę void Check(T*)
 template <class T, class ErrorHandling>
 class SmartPtr : public ErrorHandling
  {
   public:

   T * Get() const
    {
     Check(_ptr);  // <----- !!! zmienia się w zależności od ErrorHandling
     return _ptr;
    }

   private:

    T * _ptr;
 };

 // Używanie:

 struct Foo
  {
   void Bar() { }
  };

 SmartPtr<Foo, NoChecking<Foo> > ptrNoCheck = new Foo();

 ptrNoCheck.Get()->Bar();  // Get() nie sprawdzane

 SmartPtr<Foo, StrictChecking<Foo> > ptrChecked = new Foo();

 ptrChecked.Get()->Bar();  // Get() sprawdzane

Aby dodać wsparcie dla np. wielowątkowości i liczenia referencji, i jednocześnie pozostawić użytkownikowi możliwość wyboru zachowania klasy, jak również dodawania własnych „strategii zachowań”, wystarczy dodać kolejne wytyczne. W przypadku stosowania innych technik, osiągnięcie podobnych rezultatów najprawdopodobniej nie byłoby tak łatwe.

Wytyczne są techniką użyteczną szczególnie dla twórców bibliotek i silników, ponieważ umożliwiają pisanie kodu, który może być w łatwy, przenośny, efektywny i bezpieczny typologicznie sposób rozszerzany przez użytkowników.

Przypisy

edytuj
  1. Andrei Alexandrescu, Nowoczesne projektowanie w C++, WNT, 2005.

Linki zewnętrzne

edytuj