W lekcji 11.6 -- Szablony funkcji, przyjrzeliśmy się szablonom funkcji:
template <typename T> // this is the template parameter declaration
T max(T x, T y) // this is the function template definition for max<T>
{
return (x < y) ? y : x;
}Za pomocą szablonu funkcji możemy zdefiniować parametry szablonu typu (np. typename T), a następnie użyć ich jako typu naszych parametrów funkcji (T x, T y).
W lekcji 13.13 — Szablony klas, omówiliśmy szablony klas, które pozwalają nam używać parametrów szablonów typów dla typu naszych składowych danych typów klas (struktura, klasy i związki):
#include <iostream>
template <typename T>
struct Pair
{
T first{};
T second{};
};
// Here's a deduction guide for our Pair (required in C++17 or older)
// Pair objects initialized with arguments of type T and T should deduce to Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;
int main()
{
Pair<int> p1{ 5, 6 }; // instantiates Pair<int> and creates object p1
std::cout << p1.first << ' ' << p1.second << '\n';
Pair<double> p2{ 1.2, 3.4 }; // instantiates Pair<double> and creates object p2
std::cout << p2.first << ' ' << p2.second << '\n';
Pair<double> p3{ 7.8, 9.0 }; // creates object p3 using prior definition for Pair<double>
std::cout << p3.first << ' ' << p3.second << '\n';
return 0;
}Powiązana treść
Omawiamy wskazówki dotyczące dedukcji na lekcji 13.14 -- Przewodniki dedukcji i dedukcji szablonu klasy (CTAD).
W tej lekcji połączymy elementy zarówno szablonów funkcji, jak i szablonów klas, gdy przyjrzymy się bliżej szablonom klas, które mają funkcje składowe.
Parametry szablonu typu w funkcjach składowych
Parametry szablonu typów zdefiniowane jako część deklaracji parametrów szablonu klasy mogą być używane zarówno jako typ elementów danych, jak i jako typ parametrów funkcji składowej.
W poniższym przykładzie przepisujemy powyższy Pair szablon klasy, konwertując go ze struktury do klasy:
#include <ios> // for std::boolalpha
#include <iostream>
template <typename T>
class Pair
{
private:
T m_first{};
T m_second{};
public:
// When we define a member function inside the class definition,
// the template parameter declaration belonging to the class applies
Pair(const T& first, const T& second)
: m_first{ first }
, m_second{ second }
{
}
bool isEqual(const Pair<T>& pair);
};
// When we define a member function outside the class definition,
// we need to resupply a template parameter declaration
template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
return m_first == pair.m_first && m_second == pair.m_second;
}
int main()
{
Pair p1{ 5, 6 }; // uses CTAD to infer type Pair<int>
std::cout << std::boolalpha << "isEqual(5, 6): " << p1.isEqual( Pair{5, 6} ) << '\n';
std::cout << std::boolalpha << "isEqual(5, 7): " << p1.isEqual( Pair{5, 7} ) << '\n';
return 0;
}Powyższe powinno być dość proste, ale jest kilka rzeczy, na które warto zwrócić uwagę.
Po pierwsze, ponieważ nasza klasa ma prywatne elementy, nie jest agregacją i dlatego nie może używać inicjalizacji agregowanej. Zamiast tego musimy inicjować obiekty naszej klasy za pomocą konstruktora.
Ponieważ elementy danych naszej klasy są typu T, tworzymy parametry naszego konstruktora const T&, dzięki czemu użytkownik może podać wartości inicjujące tego samego typu. Ponieważ T kopiowanie może być kosztowne, bezpieczniej jest przekazywać referencje do stałych niż przez wartość.
Zauważ, że kiedy definiujemy funkcję składową w definicji szablonu klasy, nie musimy udostępniać szablonowej deklaracji parametrów dla funkcji składowej. Takie funkcje składowe domyślnie korzystają z deklaracji parametrów szablonu klasy.
Po drugie, nie potrzebujemy przewodników po dedukcjach, z którymi CTAD będzie mógł pracować. klasy nieagregacyjne. Dopasowujący konstruktor dostarcza kompilatorowi informacji potrzebnych do wydedukowania parametrów szablonu z inicjatorów.
Po trzecie, przyjrzyjmy się bliżej przypadkowi, w którym definiujemy funkcję składową dla szablonu klasy poza definicją szablonu klasy:
template <typename T>
bool Pair<T>::isEqual(const Pair<T>& pair)
{
return m_first == pair.m_first && m_second == pair.m_second;
}Ponieważ ta definicja funkcji składowej jest oddzielona od definicji szablonu klasy, musimy ponownie dostarczyć deklarację parametrów szablonu (template <typename T>), aby kompilator wiedział, czym T jest.
Ponadto, gdy definiujemy funkcję składową poza klasą, musimy zakwalifikować nazwę funkcji składowej za pomocą w pełni szablonowej nazwy szablonu klasy (Pair<T>::isEqual, nie Pair::isEqual).
Wstrzykiwane nazwy klas
W poprzedniej lekcji zauważyliśmy, że nazwa konstruktora musi odpowiadać nazwie klasy. Ale w naszej szablon klasy dla Pair<T> powyżej nazwaliśmy naszego konstruktora Pair, nie Pair<T>. W jakiś sposób to nadal działa, nawet jeśli nazwy nie są zgodne.
W zakresie klasy niekwalifikowana nazwa klasy nazywana jest wstrzykiwaną nazwą klasy. W szablonie klasy wstrzyknięta nazwa klasy służy jako skrót dla pełnego nazwa szablonu.
Ponieważ Pair jest wstrzykniętą nazwą klasy Pair<T>, w zakresie naszego szablonu klasy Pair<T> każde użycie Pair będzie traktowane tak, jakbyśmy zamiast tego napisali Pair<T> Dlatego chociaż nazwaliśmy konstruktor Pair, kompilator traktuje to tak, jakbyśmy to zrobili zamiast tego napisano Pair<T> Nazwy są teraz zgodne!
Oznacza to, że możemy również zdefiniować naszą isEqual() funkcję składową w ten sposób:
template <typename T>
bool Pair<T>::isEqual(const Pair& pair) // note the parameter has type Pair, not Pair<T>
{
return m_first == pair.m_first && m_second == pair.m_second;
}Ponieważ jest to definicja funkcji składowej Pair<T>, jesteśmy w zakresie szablonu klasy Pair<T> . Dlatego każde użycie Pair jest skrótem od Pair<T>!
Kluczowa informacja
W lekcji 13.14 -- Przewodniki dedukcji i dedukcji szablonu klasy (CTAD). Zauważyliśmy, że CTAD nie działa z parametrami funkcji (ponieważ jest to dedukcja argumentów, a nie dedukcja parametrów). Jednakże użycie wstrzykniętej nazwy klasy jako parametru funkcji jest w porządku, ponieważ jest to skrót oznaczający pełną nazwę wzorowaną na szablonie, a nie użycie CTAD.
Gdzie zdefiniować funkcje składowe dla szablonów klas poza klasą
W przypadku funkcji składowych dla szablonów klas kompilator musi zobaczyć zarówno definicję klasy (aby mieć pewność, że szablon funkcji składowej jest zadeklarowany jako część klasy), jak i definicję funkcji składowej szablonu (aby wiedzieć, jak utworzyć instancję szablonu). Dlatego zazwyczaj chcemy zdefiniować zarówno klasę, jak i jej szablony funkcji składowych w tym samym miejscu.
Gdy szablon funkcji składowej jest zdefiniowany wewnątrz definicja klasy, szablonowa definicja funkcji składowej jest częścią definicji klasy, zatem wszędzie tam, gdzie widać definicję klasy, można zobaczyć również definicję szablonu funkcji składowej. Ułatwia to sprawę (kosztem zaśmiecania definicji klasy).
Gdy szablon funkcji składowej jest zdefiniowany poza definicją klasy, zazwyczaj powinien być zdefiniowany bezpośrednio pod definicją klasy. W ten sposób wszędzie tam, gdzie widać definicję klasy, widoczne będą również definicje szablonów funkcji składowych bezpośrednio pod definicją klasy.
W typowym przypadku, gdy klasa jest zdefiniowana w pliku nagłówkowym, oznacza to, że wszelkie szablony funkcji składowych zdefiniowane poza klasą powinny być również zdefiniowane w tym samym pliku nagłówkowym, poniżej definicji klasy.
Kluczowa informacja
W lekcji 11.7 -- Szablon funkcji instancja zauważyliśmy, że funkcje utworzone niejawnie na podstawie szablonów są domyślnie wbudowane. Dotyczy to zarówno szablonów funkcji nieczłonkowskich, jak i członkowskich. Dlatego nie ma problemu z włączaniem szablonów funkcji składowych zdefiniowanych w plikach nagłówkowych do wielu plików kodu, ponieważ funkcje utworzone na podstawie tych szablonów będą domyślnie wbudowane (a linker usunie ich duplikaty).
Najlepsza praktyka
Wszystkie szablony funkcji składowych zdefiniowane poza definicją klasy powinny być zdefiniowane tuż pod definicją klasy (w tym samym pliku).
Czas quizu
Pytanie nr 1
Napisz szablon klasy o nazwie Triad, który ma 3 prywatne elementy danych z niezależnymi parametrami szablonu typu. Klasa powinna mieć konstruktor, funkcje dostępu i print() funkcję składową zdefiniowaną poza klasą.
Następujący program powinien się skompilować i uruchomić:
#include <iostream>
#include <string>
int main()
{
Triad<int, int, int> t1{ 1, 2, 3 };
t1.print();
std::cout << '\n';
std::cout << t1.first() << '\n';
using namespace std::literals::string_literals;
const Triad t2{ 1, 2.3, "Hello"s };
t2.print();
std::cout << '\n';
return 0;
}i wygenerować wynik:
[1, 2, 3] 1 [1, 2.3, Hello]
Pytanie nr 2
Jeśli usuniemy const z deklaracji i definicji funkcji print() , program nie będzie się już kompilował. Dlaczego nie?

