Menu

Szablony w C++ (templates): funkcje i klasy generyczne

Napisz kod raz i używaj go z każdym typem dzięki szablonom w C++: szablony funkcji, szablony klas, dedukcja typów i mylące błędy kompilatora, które potrafią powodować.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Napisz raz, używaj z każdym typem

Na poprzedniej stronie vector<int> był sortowany za pomocą std::sort. Ale std::sort posortuje też vector<string>, vector<double> albo tablicę twoich własnych struktur, choć nikt nie pisze osobnego sort dla każdej z nich. To nie magia ani przeciążanie. To szablon (template): jeden fragment kodu, który kompilator wykorzystuje ponownie dla dowolnego typu, jaki mu podasz.

Bez szablonów trzeba by kopiować tę samą logikę dla każdego typu. Oto ta sama funkcja maximum napisana trzy razy, czyli dokładnie to powielanie, z którym szablony mają skończyć:

int    maximum(int a, int b)       { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }

Ciała funkcji są identyczne. Różnią się tylko typy. Szablon pozwala powiedzieć "to działa dla dowolnego typu T" i napisać kod raz.

Szablony funkcji

Funkcję zamieniasz w szablon, dodając przed nią template <typename T> i używając T wszędzie tam, gdzie normalnie stałby konkretny typ.

Zauważ, że nigdzie nie ma maximum<int> ani maximum<double>. Kompilator patrzy na argumenty i sam ustala, czym ma być T: to dedukcja argumentów szablonu. Każdy kolejny typ, z którym wywołasz funkcję, sprawia, że kompilator w tle instancjonuje (generuje) osobną, konkretną funkcję.

Typ możesz też podać jawnie w nawiasach ostrych, gdy dedukcja nie ma z czego skorzystać:

W dedukcji czai się częsta pułapka. Ponieważ T musi być jednym typem, mieszanie typów argumentów ją psuje:

maximum(3, 7.5);   // BŁĄD: T to int czy double? Kompilator nie będzie zgadywał.

Naprawisz to, pisząc jawnie maximum<double>(3, 7.5), albo dając każdemu parametrowi osobny parametr typu, czym zajmiemy się za chwilę.

Kilka parametrów typu

Szablon nie jest ograniczony do jednego typu. Wypisz ich tyle, ile potrzebujesz, oddzielając je przecinkami. Tak piszesz funkcję, której parametry mogą mieć różne typy:

Gdy typ zwracany zależy od parametrów, pozwól kompilatorowi go ustalić za pomocą auto (od C++14), które świetnie współpracuje z szablonami:

Szablony klas

Szablony nie są tylko dla funkcji: całe klasy też mogą być szablonami. Dokładnie tak działają kontenery standardowe: vector<int>, mapa klucz-wartość map i pair<A, B> to szablony klas. Strukturę danych piszesz raz, a ona przechowuje dowolny typ, którym ją sparametryzujesz.

Oto malutki generyczny Box, który przechowuje jedną wartość dowolnego typu:

Najważniejsza różnica względem szablonów funkcji: przy szablonie klasy zwykle trzeba podać typ w nawiasach ostrych, Box<int>, bo w starszych standardach nie ma argumentów konstruktora, z których dałoby się go wydedukować. (C++17 dodał dedukcję argumentów szablonu klasy, więc Box b(42); też działa, ale jawny zapis jest zawsze bezpieczny i czytelny.)

Błędy będą ogromne: oto dlaczego

Na tym potyka się każdy, więc warto powiedzieć to wprost. Szablon jest w pełni sprawdzany dopiero wtedy, gdy zostanie zinstancjonowany z prawdziwym typem. Możesz napisać szablon, który używa <, i sam w sobie skompiluje się bez problemu: błąd pojawi się dopiero w chwili instancjacji z typem, który nie ma <.

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;   // wymaga, by T obsługiwał >
}

struct Point { int x, y; };

// maximum(Point{1,2}, Point{3,4});
// BŁĄD: brak operator > dla Point. Komunikat wymienia Point ORAZ
// cytuje całą tę funkcję, często na wielu liniach.

Kompilator podstawia pełny typ do szablonu i zgłasza błędy z wnętrza wygenerowanego kodu, więc jedna pomyłka może dać ścianę tekstu pełną szczegółów biblioteki. Dwie rady, jak to przetrwać:

  • Czytaj pierwszy błąd, nie ostatni. Kolejne błędy są zwykle skutkiem pierwszego.
  • Szukaj w komunikacie nazwy swojego typu (tutaj Point). Dzięki temu wiesz, która instancjacja zawiodła.

Prawdziwe rozwiązanie to zadbanie, by twój typ obsługiwał wszystko, czego potrzebuje szablon: w przypadku maximum oznacza to przeciążenie operator> dla Point, czym zajmiemy się na jednej z dalszych stron. Współczesne koncepty (concepts) z C++20 potrafią przesunąć te błędy wcześniej i uczynić je czytelnymi, ale model podstawiania pod spodem jest ten sam.

Dalej: klasy

Właśnie powstał szablon klasy Box: klasa z prywatnymi danymi, konstruktorem i funkcjami składowymi, choć uwaga skupiała się na szablonach. Następna strona zwalnia tempo i porządnie uczy klas: jak łączyć dane z funkcjami, które na nich działają, co naprawdę kontrolują public i private oraz jak funkcje składowe sięgają do stanu własnego obiektu. Szablony i klasy łączą się w prawdziwym C++ bez przerwy, więc solidne zrozumienie klas bardzo ułatwia pisanie kodu generycznego.

Najczęściej zadawane pytania

Czym jest szablon (template) w C++?

Szablon to plan, dzięki któremu piszesz funkcję lub klasę raz, a kompilator generuje jej wersję dla każdego typu, z którym jej użyjesz. Piszesz template <typename T>, a potem używasz T w miejsce prawdziwego typu. Kompilator tworzy z tego konkretną wersję: nazywa się to instancjacją.

Jaka jest różnica między typename a class w szablonie C++?

Na liście parametrów szablonu template <typename T> i template <class T> znaczą dokładnie to samo. Dziś zwykle wybiera się typename, bo lepiej oddaje sens: T może być dowolnym typem, nie tylko klasą. Wybór słowa kluczowego nie ma żadnego wpływu na wygenerowany kod.

Dlaczego komunikaty o błędach szablonów w C++ są tak długie?

Szablony są sprawdzane dopiero wtedy, gdy zostaną zinstancjonowane z prawdziwym typem, a nie w chwili pisania. Jeśli typ nie obsługuje użytej operacji (na przykład < przy sortowaniu), błąd pojawia się głęboko w kodzie biblioteki, z pełną nazwą zinstancjonowanego typu, co daje całe strony komunikatów. Czytaj pierwszy błąd i szukaj w nim nazwy swojego typu.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ