====== 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 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(2, 7); //zostanie wygenerowana metoda IsLess(const int &left, const int &right); bool r2 = IsLess(8.019, 1.901); //j.w. ale IsLess(const double &left, const double &right); bool r3 = IsLess("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 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 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 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 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 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 [[http://coliru.stacked-crooked.com/a/8fcc46231aa10ea9|tutaj]] i dla porównania ten sam kod przy użyciu interfejsu z funkcją wirtualną, gdzie programista zapomniał ją [[http://coliru.stacked-crooked.com/a/8e9895212eb3f7cb|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> &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>(span).count() * 1000; } }; Całość można zobaczyć [[http://coliru.stacked-crooked.com/a/7d241006cd2ff5e3|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 [[https://en.wikipedia.org/wiki/Concepts_(C%2B%2B)|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 void LoadAllResources(std::vector *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 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>(span).count() * 1000; } }; Całość można zanalizować [[http://coliru.stacked-crooked.com/a/fd92eb033639ca2e|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.: - operatory - konstruktory - 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 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: - InputIterator musi posiadać następujące operatory (ze względu na użycie first i last): - bool operator!=(const T &) - z powodu warunku w while - T &operator*() - ten jest wykorzystywany do dereferencji przy przekazywaniu argumentu do funkcji - operator++() - przesunięcie iteratora o jedno oczko do przodu ostatnia linia pętli - OutputIterator: - R &operator*() - dereferencja result w celu zapisania wyniku - operator++() - przesunięcie iteratora result - UnaryOperation musi przeładowywać: - R operator() (const T &) - ze względu na użycie op jako operacji - 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 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: template auto First(const T &v) { auto found = v.begin(); if (found != v.end()) { return *found; } throw std::runtime_error("not enough elements"); } ===== Dodatkowe materiały ===== [[http://www.bogotobogo.com/cplusplus/templates.php|dodatkowy opis]] ===== Ćwiczenia do wykonania ===== - [2 plusy] Metoda fabryczna to jeden z [[https://pl.wikipedia.org/wiki/Metoda_wytw%C3%B3rcza_%28wzorzec_projektowy%29|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 plusy] Przygotować metodę sumującą dwa obiekty dowolnego typu: np. double, int, complex - [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 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... - [2 plusy] Zdefiniować metodę Mean, która wyliczy średnią artytmentyczną dla różnych vectorów (int, double, complex)... - [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.