W lekcji 11.6 -- Szablony funkcji, wprowadziliśmy wyzwanie polegające na konieczności utworzenia osobnej (przeciążonej) funkcji dla każdego innego zestawu typów, z którymi chcemy pracować:
#include <iostream>
// funkcja do obliczenia większej z dwóch int wartości
int max(int x, int y)
{
return (x < y) ? y : x;
}
// prawie identycznej funkcji do obliczenia większej z dwóch wartości double
// jedyną różnicą jest informacja o typie
double max(double x, double y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // calls max(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // calls max(double, double)
return 0;
}Rozwiązaniem tego było utworzenie szablonu funkcji, którego kompilator może użyć do utworzenia instancji normalnych funkcji dla dowolnego zestawu typów, których potrzebujemy:
#include <iostream>
// szablon adaptera funkcji dla max
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max(5, 6); // tworzy instancje i wywołuje max<int>(int, int)
std::cout << '\n';
std::cout << max(1.2, 3.4); // tworzy instancje i wywołuje max<double>(double, double)
return 0;
}Powiązana treść
W lekcji 11.7 -- Szablon funkcji instancja.
Typy agregatów wiążą się z podobnymi wyzwaniami
Napotykamy podobne wyzwania z typami agregacji (zarówno strukturami/klasami/unionami, jak i tablicami).
Załóżmy na przykład, że piszemy program, w którym musimy pracować z parami int wartości i musimy określić, która z dwóch liczb jest większa. Moglibyśmy napisać taki program:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
constexpr int max(Pair p) // przekaż wartość, ponieważ Para jest mała
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair p1{ 5, 6 };
std::cout << max(p1) << " is larger\n";
return 0;
}Później odkrywamy, że potrzebujemy także par double wartości. Dlatego aktualizujemy nasz program do następującego:
#include <iostream>
struct Pair
{
int first{};
int second{};
};
struct Pair // błąd kompilacji: błędna redefinicja Para
{
double first{};
double second{};
};
constexpr int max(Pair p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair p) // błąd kompilacji: funkcja przeciążona różni się tylko typem zwracanym
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair p1{ 5, 6 };
std::cout << max(p1) << " is larger\n";
Pair p2{ 1.2, 3.4 };
std::cout << max(p2) << " is larger\n";
return 0;
}Niestety ten program się nie skompiluje i ma wiele problemów, które należy rozwiązać.
Po pierwsze, w przeciwieństwie do funkcji, definicje typów nie mogą być przeciążane. Kompilator potraktuje definicję podwójnej sekundy Pair jako błędną ponowną deklarację pierwszej definicji Pair. Po drugie, chociaż funkcje mogą być przeciążane, nasze max(Pair) funkcje różnią się jedynie typem zwracanej wartości, a funkcji przeciążonych nie można różnicować wyłącznie na podstawie typu zwracanego. Po trzecie, jest tu dużo nadmiarowości. Każda Pair struktura jest identyczna (z wyjątkiem typu danych) i taka sama z naszymi max(Pair) funkcjami (z wyjątkiem typu zwracanego).
Moglibyśmy rozwiązać pierwsze dwa problemy, nadając naszym Pair strukturom różne nazwy (np. PairInt i PairDouble). Ale wtedy oboje musimy zapamiętać nasz schemat nazewnictwa i zasadniczo sklonować trochę kodu dla każdego dodatkowego typu pary, jaki chcemy, co nie rozwiązuje problemu redundancji.
Na szczęście możemy zrobić lepiej.
Nota autora
Przed kontynuowaniem przejrzyj lekcje 11.6 -- Szablony funkcji i 11.7 -- Szablon funkcji instancja jeśli nie masz pewności, jak działają szablony funkcji, typy szablonów lub tworzenie instancji szablonów funkcji.
Szablony klas
Podobnie jak szablon funkcji jest definicją szablonu do tworzenia instancji funkcji, Szablon klasy jest definicją szablonu do tworzenia instancji typów klas.
Przypomnienie
„Typ klasy” to struktura, klasa lub typ unii. Chociaż dla uproszczenia zademonstrujemy „szablony klas” na strukturach, wszystko tutaj można zastosować równie dobrze do klas.
Dla przypomnienia, oto nasza int definicja struktury par:
struct Pair
{
int first{};
int second{};
};Przepiszmy naszą klasę par jako szablon klasy:
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // tworzy instancję Pair<int> i tworzy obiekt p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // tworzy instancję Pair<double> i tworzy obiekt p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // tworzy obiekt p3 korzystając z wcześniejszej definicji pary<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}Podobnie jak w przypadku szablonów funkcji, definicję szablonu klasy zaczynamy od deklaracji parametrów szablonu. Zaczynamy od słowa kluczowego template . Następnie w nawiasach ostrych (<>) określamy wszystkie typy szablonów, których będzie używał nasz szablon klasy. Dla każdego typu szablonu, którego potrzebujemy, używamy słowa kluczowego typename (preferowany) lub class (niepreferowany), po którym następuje nazwa typu szablonu (np. T). W tym przypadku, ponieważ obaj nasi członkowie będą tego samego typu, potrzebujemy tylko jednego typu szablonu.
Następnie definiujemy naszą strukturę jak zwykle, z tą różnicą, że możemy użyć naszego typu szablonu (T) wszędzie tam, gdzie chcemy typu szablonowego, który zostanie później zastąpiony typem rzeczywistym. To wszystko! Skończyliśmy z definicją szablonu klasy.
W main możemy tworzyć instancje Pair obiektów przy użyciu dowolnych typów. Najpierw tworzymy instancję obiektu typu Pair<int>. Ponieważ definicja typu dla Pair<int> jeszcze nie istnieje, kompilator używa szablonu klasy do utworzenia instancji definicji typu struktury o nazwie Pair<int>, gdzie wszystkie wystąpienia typu szablonu T są zastępowane przez typ int.
Następnie tworzymy instancję obiektu typu Pair<double>, który tworzy instancję definicji typu struktury o nazwie Pair<double> gdzie T jest zastępowana przez double. Instancja for p3, Pair<double> została już utworzona, więc kompilator użyje wcześniejszej definicji typu.
Oto ten sam przykład co powyżej, pokazujący, co kompilator faktycznie kompiluje po wykonaniu wszystkich instancji szablonu:
#include <iostream>
// Deklaracja naszego szablonu klasy Pair
// (nie potrzebujemy już definicji, ponieważ nie jest używana)
template <typename T>
struct Pair;
// Explicitly define what Pair<int> looks like
template <> // mówi kompilatorowi, że jest to typ szablonu bez parametrów szablonu
struct Pair<int>
{
int first{};
int second{};
};
// Explicitly define what Pair<double> looks like
template <> // mówi kompilatorowi, że jest to typ szablonu bez parametrów szablonu
struct Pair<double>
{
double first{};
double second{};
};
int main()
{
Pair<int> p1{ 5, 6 }; // tworzy instancję Pair<int> i tworzy obiekt p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // tworzy instancję Pair<double> i tworzy obiekt p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // tworzy obiekt p3 korzystając z wcześniejszej definicji pary<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}Możesz skompilować ten przykład bezpośrednio i sprawdzić, czy działa zgodnie z oczekiwaniami!
Dla zaawansowanych czytelników
Powyższy przykład wykorzystuje funkcję zwaną specjalizacją szablonów klas (omówioną w przyszłości lekcja 26.4 -- Specjalizacja szablonu zajęć). Na tym etapie nie jest wymagana wiedza na temat działania tej funkcji.
Wykorzystanie naszego szablonu klasy w funkcji
Powróćmy teraz do wyzwania, jakim jest sprawienie, aby nasza max() funkcja działała z różnymi typami. Ponieważ kompilator traktuje Pair<int> i Pair<double> jako oddzielne typy, możemy użyć przeciążonych funkcji, które różnią się typem parametru:
constexpr int max(Pair<int> p)
{
return (p.first < p.second ? p.second : p.first);
}
constexpr double max(Pair<double> p) // ok: przeciążona funkcja zróżnicowana według typu parametru
{
return (p.first < p.second ? p.second : p.first);
}Podczas kompilacji nie rozwiązuje to problemu redundancji. Tak naprawdę potrzebujemy funkcji, która może przyjąć parę dowolnego typu. Innymi słowy, chcemy funkcji, która przyjmuje parametr typu Pair<T>, gdzie T jest parametrem typu szablonu. A to oznacza, że potrzebujemy szablonu funkcji do tego zadania!
Oto pełny przykład z max() zaimplementowanym jako szablon funkcji:
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}
int main()
{
Pair<int> p1{ 5, 6 };
std::cout << max<int>(p1) << " is larger\n"; // jawne wywołanie max<int>
Pair<double> p2{ 1.2, 3.4 };
std::cout << max(p2) << " is larger\n"; // wywołaj max<double> używając odliczenia argumentów szablonu (preferuj to)
return 0;
}Klasa max() szablon funkcji jest całkiem prosty. Ponieważ chcemy przekazać Pair<T>, kompilator musi zrozumieć, co T to jest. Dlatego musimy rozpocząć naszą funkcję od deklaracji parametrów szablonu, która definiuje typ szablonu T. Możemy następnie użyć T jako naszego typu zwracanego i typu szablonu dla Pair<T>.
Gdy max() funkcja zostanie wywołana z argumentem Pair<int> , kompilator utworzy instancję funkcji int max<int>(Pair<int>) z szablonu funkcji, gdzie szablon typ T zastępuje się int. Poniższy fragment pokazuje, co kompilator faktycznie tworzy w takim przypadku:
template <>
constexpr int max(Pair<int> p)
{
return (p.first < p.second ? p.second : p.first);
}Jak w przypadku wszystkich wywołań szablonu funkcji, możemy albo wyraźnie określić argument typu szablonu (np. max<int>(p1)), albo możemy to zrobić niejawnie (np. max(p2)) i pozwolić kompilatorowi użyć dedukcji argumentów szablonu, aby określić, jaki powinien być argument typu szablonu.
Szablony klas z elementami typu szablonowego i elementami niebędącymi szablonami
Szablony klas mogą mieć niektóre elementy korzystające z typu szablonowego, a inne elementy korzystające z typu normalnego (niebędącego szablonem). Na przykład:
template <typename T>
struct Foo
{
T first{}; // najpierw będzie dowolny typ T zostaje zastąpiony przez
int second{}; // sekunda zawsze będzie miała typ int, niezależnie od typu T
};Działa to dokładnie tak, jak można się spodziewać: first będzie niezależnie od typu szablonu T i second zawsze będzie int.
szablonem klas z wieloma typami szablonów
Szablony klas mogą również mieć wiele typów szablonów. Na przykład, jeśli chcemy, aby dwaj członkowie naszej klasy Pair mogli mieć różne typy, możemy zdefiniować nasz szablon klasy Pair z dwoma typami szablonów:
#include <iostream>
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
template <typename T, typename U>
void print(Pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}
int main()
{
Pair<int, double> p1{ 1, 2.3 }; // para zawierająca int i double
Pair<double, int> p2{ 4.5, 6 }; // para zawierająca double i int
Pair<int, int> p3{ 7, 8 }; // para zawierająca dwa inty
print(p2);
return 0;
}Aby zdefiniować wiele typów szablonów, w naszej deklaracji parametrów szablonu oddzielamy każdy z pożądanych typów szablonów przecinkiem. W powyższym przykładzie definiujemy dwa różne typy szablonów, jeden o nazwie T i drugi o nazwie U. Rzeczywiste argumenty typu szablonu dla T i U mogą być różne (jak w przypadku p1 i p2 powyżej) lub takie same (jak w przypadku p3).
Sprawienie, aby szablon funkcji działał z więcej niż jednym typem klasy
Rozważ print() szablon funkcji z powyższego przykładu:
template <typename T, typename U>
void print(Pair<T, U> p)
{
std::cout << '[' << p.first << ", " << p.second << ']';
}Ponieważ jawnie zdefiniowaliśmy parametr funkcji jako Pair<T, U>, tylko argumenty typu Pair<T, U> (lub takie, które można przekonwertować na Pair<T, U>) będzie pasować. Jest to idealne rozwiązanie, jeśli chcemy móc wywoływać naszą funkcję tylko z Pair<T, U> argumentem.
W niektórych przypadkach możemy napisać szablony funkcji, których chcemy używać z dowolnym typem, który pomyślnie się skompiluje. Aby to zrobić, po prostu używamy parametru szablonu typu jako parametru funkcji.
Na przykład:
#include <iostream>
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
struct Point
{
int first{};
int second{};
};
template <typename T>
void print(T p) // parametr szablonu typu będzie pasował do wszystkiego
{
std::cout << '[' << p.first << ", " << p.second << ']'; // zostanie skompilowany tylko wtedy, gdy typ ma pierwszego i drugiego członka
}
int main()
{
Pair<double, int> p1{ 4.5, 6 };
print(p1); // dopasowuje print(Pair<double, int>)
std::cout << '\n';
Point p2 { 7, 8 };
print(p2); // dopasowuje print(Point)
std::cout << '\n';
return 0;
}W powyższym przykładzie przerobiliśmy print() tak, aby miał tylko jeden parametr szablonu typu (T), który będzie pasował do dowolnego typu. Treść funkcji zostanie pomyślnie skompilowana dla dowolnego typu klasy, który ma element first i second . Pokazujemy to wywołując print() z obiektem typu Pair<double, int>, a następnie ponownie z obiektem typu Point.
Jest jeden przypadek, który może wprowadzić w błąd. Rozważ następującą wersję print():
template <typename T, typename U>
struct Pair // Określenie typ klasy o nazwie Pair
{
T first{};
U second{};
};
template <typename Pair> // definiuje parametr szablonu typu o parametrze Para (cienie Typ klasy Pair)
void print(Pair p) // to odnosi się do parametru szablonu Para, a nie typ klasy Para
{
std::cout << '[' << p.first << ", " << p.second << ']';
}Można się spodziewać, że ta funkcja będzie pasować tylko wtedy, gdy zostanie wywołana z Pair argumentem typu klasy. Ale ta wersja print() jest funkcjonalnie identyczna z poprzednią wersją, w której parametr szablonu został nazwany T i będzie pasować do dowolnego typu. Problem polega na tym, że gdy zdefiniujemy Pair jako parametr szablonu typu, przesłania to inne zastosowania nazwy Pair w zakresie globalnym. Zatem w szablonie funkcji Pair odnosi się do parametru szablonu Pair, a nie do typu klasy Pair. A ponieważ parametr szablonu typu będzie pasował do dowolnego typu, Pair pasuje do dowolnego typu argumentów, a nie tylko typu klasowego Pair!
Jest to dobry powód, aby trzymać się prostych nazw parametrów szablonu, takich jak T, U, N, ponieważ jest mniej prawdopodobne, że będą one przesłaniały nazwę typu klasy.
std::pair
Ponieważ praca z parami danych jest powszechna, Biblioteka standardowa C++ zawiera szablon klasy o nazwie std::pair (w <utility> nagłówku), który jest zdefiniowany identycznie jak szablon klasy Pair z wieloma typami szablonów w poprzedniej sekcji. W rzeczywistości możemy zamienić pair strukturę, którą opracowaliśmy dla std::pair:
#include <iostream>
#include <utility>
template <typename T, typename U>
void print(std::pair<T, U> p)
{
// elementy std::pair mają predefiniowane nazwy `pierwszy` i `drugi`
std::cout << '[' << p.first << ", " << p.second << ']';
}
int main()
{
std::pair<int, double> p1{ 1, 2.3 }; // para zawierająca int i double
std::pair<double, int> p2{ 4.5, 6 }; // para zawierająca double i int
std::pair<int, int> p3{ 7, 8 }; // para zawierająca dwa inty
print(p2);
return 0;
}W tej lekcji opracowaliśmy własną Pair klasę, aby pokazać, jak to działa, ale w prawdziwym kodzie należy preferować std::pair zamiast pisania własnych.
Korzystanie z szablonów klas w wielu plikach
Podobnie jak szablony funkcji, szablony klas są zwykle definiowane w nagłówkowe, dzięki czemu można je dołączyć do dowolnego pliku kodu, który ich potrzebuje. Zarówno definicje szablonów, jak i definicje typów są wyłączone z reguły jednej definicji, więc nie spowoduje to problemów:
pair.h:
#ifndef PAIR_H
#define PAIR_H
template <typename T>
struct Pair
{
T first{};
T second{};
};
template <typename T>
constexpr T max(Pair<T> p)
{
return (p.first < p.second ? p.second : p.first);
}
#endiffoo.cpp:
#include "pair.h"
#include <iostream>
void foo()
{
Pair<int> p1{ 1, 2 };
std::cout << max(p1) << " is larger\n";
}main.cpp:
#include "pair.h"
#include <iostream>
void foo(); // deklaracja forward dla funkcji foo()
int main()
{
Pair<double> p2 { 3.4, 5.6 };
std::cout << max(p2) << " is larger\n";
foo();
return 0;
}
