Szablony w C++

Getting started

Przy pomocy szablonów możemy zdefiniować przepis na funkcję, albo klasę. Na podstawie tego szablonu kompilator generuje następnie właściwy kod metod pod każdy przypadek użycia napotkany w kodzie. Szablon metody IsLess:

template<class T>
bool IsLess( const T &left, const T &right) {
  return left < right;
}

To jest tylko przepis jak zrobić funkcję IsLess dla dowolnego typu, jedyny wymóg jaki jest to ten typ musi mieć zdefiniowany operator<, bo taki jest wykorzystywany w kodzie metody.

Niestety ponieważ IDE nie ma pojęcia co będzie w przyszłości podstawione pod typ T nie będzie nam podpowiadało jakie metody są dostępne dla tego typu (tutaj jest przewaga typów opartych na interfejsach).

Przykład użycia:

bool result = IsLess<int>(2, 7); //zostanie wygenerowana metoda IsLess(const int &left, const int &right);
bool r2 = IsLess<double>(8.019, 1.901); //j.w. ale IsLess(const double &left, const double &right);
bool r3 = IsLess<string>("abc", "efg"); //IsLess(const string &left, const string &right);
//w ogole mozna pominac typ szablonu i pozwolić sie domyśleć 
//kompilatorowi o jaka wersje metody nam chodzi:
bool r4 = IsLess(55, 99);
bool r5 = IsLess(StudentId {4}, StudentId{7}); //będzie działać pod warunkiem, że klasa StudentId ma przeładowany operator<

Niestety jeśli coś pójdzie źle np. podamy do metody szablonowej obiekt, który nie ma zdefiniowanego operator< kompilator zasypie nas mnóstwem błędów (jak już pewnie wiecie =) )

class MyType {
//nie ma zdefiniowanego operatora<    
};
 
int main() {
   MyType m1 {};
   MyType m2 {};
   bool r_error = IsLess(m1, m2);
   cout << r_error << endl;
}

Na co kompilator może zareagować w ten sposób:

main.cpp: In instantiation of 'bool IsLess(T, T) [with T = MyType]':

main.cpp:23:32:   required from here

main.cpp:12:11: error: no match for 'operator<' (operand types are 'MyType' and 'MyType')

  return a < b;

         ~~^~~

In file included from /usr/local/include/c++/7.1.0/bits/stl_algobase.h:64:0,

                 from /usr/local/include/c++/7.1.0/bits/char_traits.h:39,

                 from /usr/local/include/c++/7.1.0/ios:40,

                 from /usr/local/include/c++/7.1.0/ostream:38,

                 from /usr/local/include/c++/7.1.0/iostream:39,

                 from main.cpp:1:

/usr/local/include/c++/7.1.0/bits/stl_pair.h:449:5: note: candidate: template<class _T1, class _T2> constexpr bool std::operator<(const std::pair<_T1, _T2>&, const std::pair<_T1, _T2>&)

     operator<(const pair<_T1, _T2>& __x, const pair<_T1, _T2>& __y)

     ^~~~~~~~

/usr/local/include/c++/7.1.0/bits/stl_pair.h:449:5: note:   template argument deduction/substitution failed:

main.cpp:12:11: note:   'MyType' is not derived from 'const std::pair<_T1, _T2>'

  return a < b;

         ~~^~~

In file included from /usr/local/include/c++/7.1.0/bits/stl_algobase.h:67:0,

                 from /usr/local/include/c++/7.1.0/bits/char_traits.h:39,

                 from /usr/local/include/c++/7.1.0/ios:40,

                 from /usr/local/include/c++/7.1.0/ostream:38,

                 from /usr/local/include/c++/7.1.0/iostream:39,

                 from main.cpp:1:

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:305:5: note: candidate: template<class _Iterator> bool std::operator<(const std::reverse_iterator<_Iterator>&, const std::reverse_iterator<_Iterator>&)

     operator<(const reverse_iterator<_Iterator>& __x,

     ^~~~~~~~

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:305:5: note:   template argument deduction/substitution failed:

main.cpp:12:11: note:   'MyType' is not derived from 'const std::reverse_iterator<_Iterator>'

  return a < b;

         ~~^~~

In file included from /usr/local/include/c++/7.1.0/bits/stl_algobase.h:67:0,

                 from /usr/local/include/c++/7.1.0/bits/char_traits.h:39,

                 from /usr/local/include/c++/7.1.0/ios:40,

                 from /usr/local/include/c++/7.1.0/ostream:38,

                 from /usr/local/include/c++/7.1.0/iostream:39,

                 from main.cpp:1:

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:343:5: note: candidate: template<class _IteratorL, class _IteratorR> bool std::operator<(const std::reverse_iterator<_Iterator>&, const std::reverse_iterator<_IteratorR>&)

     operator<(const reverse_iterator<_IteratorL>& __x,

     ^~~~~~~~

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:343:5: note:   template argument deduction/substitution failed:

main.cpp:12:11: note:   'MyType' is not derived from 'const std::reverse_iterator<_Iterator>'

  return a < b;

         ~~^~~

In file included from /usr/local/include/c++/7.1.0/bits/stl_algobase.h:67:0,

                 from /usr/local/include/c++/7.1.0/bits/char_traits.h:39,

                 from /usr/local/include/c++/7.1.0/ios:40,

                 from /usr/local/include/c++/7.1.0/ostream:38,

                 from /usr/local/include/c++/7.1.0/iostream:39,

                 from main.cpp:1:

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:1142:5: note: candidate: template<class _IteratorL, class _IteratorR> bool std::operator<(const std::move_iterator<_IteratorL>&, const std::move_iterator<_IteratorR>&)

     operator<(const move_iterator<_IteratorL>& __x,

     ^~~~~~~~

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:1142:5: note:   template argument deduction/substitution failed:

main.cpp:12:11: note:   'MyType' is not derived from 'const std::move_iterator<_IteratorL>'

  return a < b;

         ~~^~~

In file included from /usr/local/include/c++/7.1.0/bits/stl_algobase.h:67:0,

                 from /usr/local/include/c++/7.1.0/bits/char_traits.h:39,

                 from /usr/local/include/c++/7.1.0/ios:40,

                 from /usr/local/include/c++/7.1.0/ostream:38,

                 from /usr/local/include/c++/7.1.0/iostream:39,

                 from main.cpp:1:

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:1148:5: note: candidate: template<class _Iterator> bool std::operator<(const std::move_iterator<_IteratorL>&, const std::move_iterator<_IteratorL>&)

     operator<(const move_iterator<_Iterator>& __x,

     ^~~~~~~~

/usr/local/include/c++/7.1.0/bits/stl_iterator.h:1148:5: note:   template argument deduction/substitution failed:

main.cpp:12:11: note:   'MyType' is not derived from 'const std::move_iterator<_IteratorL>'

  return a < b;

         ~~^~~

I jeszcze ponad setka linii tego typu

Kod jest tutaj i dla porównania ten sam kod przy użyciu interfejsu z funkcją wirtualną, gdzie programista zapomniał ją przesłonić

a tak naprawdę najbardziej istotne są te linie:

main.cpp: In instantiation of 'bool IsLess(T, T) [with T = MyType]':
 
main.cpp:23:32:   required from here
 
main.cpp:12:11: error: no match for 'operator<' (operand types are 'MyType' and 'MyType')
 
  return a < b;
 
         ~~^~~

Pierwsza mówi, że w przypadku próby stworzenia funkcji IsLess dla typu T = MyType, druga mówi, w której linii nastąpił ten błąd (tutaj 23) „required from here”.

i kolejna mówiąca czemu ten typ nie może zostać użyty (brak operator<).

interfejsy i dziedziczenie vs szablony

Za pomocą dziedziczenia można uzyskać taki sam efekt jak za pomocą szablonów (z paroma wyjątkami po jednej i drugiej stronie). Poniżej pokazany jest przykład właśnie takiego problemu, który można rozwiązać i tak i tak:

Przykład z dziedziczeniem

Wyobraźmy sobie, że mamy aplikację, która może zarządzać różnymi zasobami, np. ładować dane ze strony w Internecie, ładować pliki z dysku lokalnego lub z chmury itp… Dlatego w kodzie została wprowadzona abstrakcja w postaci interfejsu Loadable:

class Loadable {
 public:
  virtual void Load() =0;
  virtual ~Loadable() {}
};

a konkretna klasa mogłaby wyglądać następująco:

class LocalFile : public Loadable {
 public:
  LocalFile(const std::string &path) :path_{path} {}
  ~LocalFile() {file_.close(); }
 
  void Load() override {
    file_.open(path_);
  }
 
  virtual std::ostream &Out() {
    return file_;
  }
 
  virtual std::istream &In() {
    return file_;
  }
 
 private:
  std::string path_;  
  std::fstream file_;    
};

Z kolei kod wykorzystujący zasoby mógłby wyglądać następująco:

void LoadAllResources(const std::vector<std::reference_wrapper<Loadable>> &resources) {
    for (Loadable &resource : resources) {
       resource.Load();
    }
}

lub można by było stworzyć „opakowywacz”, który będzie współpracował z innymi obiektami Loadable i notował czas ich ładowania:

class LoggerLoader : public Loadable {
 public:
  LoggerLoader(Loadable *resource, std::ostream *logger) : resource_{resource}, logger_{*logger} {}
 
  void Load() override {
   auto before = Now();
   resource_->Load();
   auto after = Now();
   logger_ << "loading length: " << ToMiliseconds(after-before) << std::endl;
  }
 
 private:
  Loadable *resource_;
  std::ostream &logger_;
 
  static std::chrono::high_resolution_clock::time_point Now() {
      return std::chrono::high_resolution_clock::now();
  }
 
  static double ToMiliseconds(std::chrono::high_resolution_clock::duration span) {
      return std::chrono::duration_cast<std::chrono::duration<double>>(span).count() * 1000;
  }
};

Całość można zobaczyć tutaj.

Wersja z szablonami

W każdym razie cały czas bazujemy na kontrakcie jaki stanowi interfejs (klasa bazowa) Loadable.

Szablony jednak pozwalają pisać kod bez jawnego specyfikowania kontraktu (przynajmniej do czasu wprowadzenia concetptów). Powyższy kod wyglądał by następująco:

Zamiast pierwszego fragmentu nie będzie nic:

//nie istnieje

Kod klasy teraz będzie zmodyfikowany o brak klasy bazowej:

class LocalFile {
 public:
  LocalFile(const std::string &path) :path_{path} {}
  ~LocalFile() {file_.close(); }
 
  void Load() {
    file_.open(path_);
  }
 
  std::ostream &Out() {
    return file_;
  }
 
  std::istream &In() {
    return file_;
  }
 
 private:
  std::string path_;  
  std::fstream file_;    
};

Ale do tej pory prawie nic się nie zmieniło, szablony dopiero specyfikuje się przy próbie użycia kodu:

template<class Loadable>
void LoadAllResources(std::vector<Loadable> *resources) {
    for (Loadable &resource : *resources) {
       resource.Load();
    }
}

I wszystko gra dopóki do metody LoadAllResources podamy vector referencji do dowolnego obiektu, który posiada metodę Load(). Jeszcze ostatni fragment:

template<class Loadable>
class LoggerLoader {
 public:
  LoggerLoader(Loadable *resource, std::ostream *logger) : resource_{resource}, logger_{*logger} {}
 
  void Load() override {
   auto before = Now();
   resource_->Load();
   auto after = Now();
   logger_ << "loading length: " << ToMiliseconds(after-before) << std::endl;
  }
 
 private:
  Loadable *resource_;
  std::ostream &logger_;
 
  static std::chrono::high_resolution_clock::time_point Now() {
      return std::chrono::high_resolution_clock::now();
  }
 
  static double ToMiliseconds(std::chrono::high_resolution_clock::duration span) {
      return std::chrono::duration_cast<std::chrono::duration<double>>(span).count() * 1000;
  }
};

Całość można zanalizować tutaj.

Zalety szablonów

No więc po co nam szablony jeśli informacje o błędach są mało czytelne i IDE nie jest w stanie nam pomóc pisać kodu :?:

Szablony się przydadzą wszędzie tam gdzie nie da się zdefiniować funkcji wirtualnych, czyli np.:

  1. operatory
  2. konstruktory
  3. typy prymitywne

Operatory

Przykład z operatorem był pokazany jako wstępniak. Innym przykładem jest np. wykorzystanie „klasy iteratora”. Tak naprawdę to w standardowej bibliotece wszystkie algorytmy są po definiowane jako szablony:

template <class InputIterator, class OutputIterator, class UnaryOperator>
OutputIterator transform (InputIterator first, InputIterator last,
                            OutputIterator result, UnaryOperator op)
{
  while (first != last) {
    *result = op(*first); 
    ++result; 
    ++first;
  }
  return result;
}

Tutaj został wykorzystany metoda, gdzie są trzy różne typy templatowe:

  1. InputIterator musi posiadać następujące operatory (ze względu na użycie first i last):
    1. bool operator!=(const T &) - z powodu warunku w while
    2. T &operator*() - ten jest wykorzystywany do dereferencji przy przekazywaniu argumentu do funkcji
    3. operator++() - przesunięcie iteratora o jedno oczko do przodu ostatnia linia pętli
  2. OutputIterator:
    1. R &operator*() - dereferencja result w celu zapisania wyniku
    2. operator++() - przesunięcie iteratora result
  3. UnaryOperation musi przeładowywać:
    1. R operator() (const T &) - ze względu na użycie op jako operacji
    2. lub być wskaźnikiem na funkcję: R (*op)(const T&)

Ostatni przykład pokazuje magię szablonów, nie ważne jaki jest typ obiektu, ważne co udostępnia, to może być struktura danych z przeładowanym wywołania funkcji operator() lub wskaźnik do funkcji! Nie ważne co to jest byleby się udało wywołać operację: auto result = op(arg);

Konstrukory

Przykład będzie w ćwiczeniach.

Typy prymitywne i w ogóle typy

Szczególnym przypadkiem kontraktu jest kontrakt bez użycia żadnych metod. Każdy typ będzie go spełniał, ale przydaje się ten trick jeśli chcemy zbudować kontener zawierający obiektu jednego typu, ale nie interesuje nas jakiego konkretnie!

Np. klasa Array:

template<class T>
class Array {
 public: 
  Array(size_t sz) : array_{new T[sz]} {}
  ~Array() {
    delete[] array_;
  }
 
  T &operator[](size_t index) {
     return array_[index];
  }
 
 private:
  T *array_;
};

Nie używamy żadnej metody na typie szablonowym T.

Tips & Tricks

Typ auto jako zwracana wartość z funkcji

Jeśli ciężko jest ustalić typ zwracany z funkcji, a zależy on bezpośrednio od typu parametru tylko jest jakąś jego pochodną, funkcję można wyspecyfikować z typem zwracanym auto i kompilator sam wywnioskuje o informacje typie na podstawie wartości zwracanej:



Dodatkowe materiały

Ćwiczenia do wykonania

  1. [2 plusy] Metoda fabryczna to jeden z wzorców projektowych (czyli standardowych rozwiązań standardowych problemów). Zadaniem do wykonania jest stworzenie metody szablonowej, będącej w stanie utworzyć dowolny obiekt pod warunkiem, że posiada on domyślny konstruktor
  2. [2 plusy] Przygotować metodę sumującą dwa obiekty dowolnego typu: np. double, int, complex<double>
  3. [2 plusy] Przygotować metodę Value, która będzie w stanie wyciągnąć wartość wskazywaną przez obiekt, raw pointer, shared_ptr, weak_ptr, iterator
  4. [4 plusy] Przygotować interfejs Reposiotry, który udostępnia metody (Size, operator[], NextId(), FindBy(Query)), ale jest wstanie być zdefiniowany dla dowolnego typu Building, Student, Course, itd…
  5. [2 plusy] Zdefiniować metodę Mean, która wyliczy średnią artytmentyczną dla różnych vectorów (int, double, complex)…
  6. [4 plusy] Zdefiniować klasę Logger, która przyjmuje dowolny obiekt do zapisu (udostępniający opeartor«) i zawierającą metody Debug, Info, Warning, Error przyjmujące dowolny parametr, który może być wrzucony do obiektu zapisu (przy pomocy operatora«). Zdefiniować swój własny bufor do testów.
pl/dydaktyka/jimp2/2017/labs/templates.txt · ostatnio zmienione: 2019/06/27 15:50 (edycja zewnętrzna)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0