13.13 — Szablony klas

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>

// function to calculate the greater of two int values
int max(int x, int y)
{
    return (x < y) ? y : x;
}

// almost identical function to calculate the greater of two double values
// the only difference is the type information
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>

// a single function template for max
template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(5, 6);     // instantiates and calls max<int>(int, int)
    std::cout << '\n';
    std::cout << max(1.2, 3.4); // instantiates and calls 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) // pass by value because Pair is small
{
    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 // compile error: erroneous redefinition of Pair
{
    double first{};
    double second{};
};

constexpr int max(Pair p)
{
    return (p.first < p.second ? p.second : p.first);
}

constexpr double max(Pair p) // compile error: overloaded function differs only by return type
{
    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, a 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 };        // 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;
}

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>

// A declaration for our Pair class template
// (we don't need the definition any more since it's not used)
template <typename T>
struct Pair;

// Explicitly define what Pair<int> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<int>
{
    int first{};
    int second{};
};

// Explicitly define what Pair<double> looks like
template <> // tells the compiler this is a template type with no template parameters
struct Pair<double>
{
    double first{};
    double second{};
};

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;
}

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) // okay: overloaded function differentiated by parameter type
{
    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"; // explicit call to max<int>

    Pair<double> p2{ 1.2, 3.4 };
    std::cout << max(p2) << " is larger\n"; // call to max<double> using template argument deduction (prefer this)

    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{};    // first will have whatever type T is replaced with
    int second{}; // second will always have type int, regardless of what type T is
};

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 }; // a pair holding an int and a double
    Pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
    Pair<int, int> p3{ 7, 8 };      // a pair holding two ints

    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 a 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) // type template parameter will match anything
{
    std::cout << '[' << p.first << ", " << p.second << ']'; // will only compile if type has first and second members
}

int main()
{
    Pair<double, int> p1{ 4.5, 6 };
    print(p1); // matches print(Pair<double, int>)

    std::cout << '\n';

    Point p2 { 7, 8 };
    print(p2); // matches 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 // defines a class type named Pair
{
    T first{};
    U second{};
};

template <typename Pair> // defines a type template parameter named Pair (shadows Pair class type)
void print(Pair p)       // this refers to template parameter Pair, not class type Pair
{
    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)
{
    // the members of std::pair have predefined names `first` and `second`
    std::cout << '[' << p.first << ", " << p.second << ']';
}

int main()
{
    std::pair<int, double> p1{ 1, 2.3 }; // a pair holding an int and a double
    std::pair<double, int> p2{ 4.5, 6 }; // a pair holding a double and an int
    std::pair<int, int> p3{ 7, 8 };      // a pair holding two ints

    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);
}

#endif

foo.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(); // forward declaration for function foo()

int main()
{
    Pair<double> p2 { 3.4, 5.6 };
    std::cout << max(p2) << " is larger\n";

    foo();

    return 0;
}
guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
128 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze