11.7 — Tworzenie instancji szablonu funkcji

W poprzedniej lekcji (11.6 -- Szablony funkcji), wprowadziliśmy szablony funkcji i przekonwertowaliśmy normalną max() funkcję na funkcję max<T> szablon:

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

W tej lekcji skupimy się na tym, jak używane są szablony funkcji.

Korzystanie z szablonu funkcji

Szablony funkcji nie są w rzeczywistości funkcjami — ich kod nie jest kompilowany ani wykonywany bezpośrednio. Zamiast tego szablony funkcji mają jedno zadanie: generować funkcje (które są kompilowane i wykonywane).

Aby skorzystać z naszego max<T> szablonu funkcji, możemy wykonać wywołanie funkcji z następującą składnią:

max<actual_type>(arg1, arg2); // actual_type is some actual type, like int or double

Wygląda to bardzo podobnie do normalnego wywołania funkcji - podstawowa różnica polega na dodaniu typu w nawiasach ostrych (tzw. a argumencie szablonu), który określa rzeczywisty typ, który zostanie użyty zamiast typu szablonu T.

Przyjrzyjmy się temu na prostym przykładzie:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n'; // instantiates and calls function max<int>(int, int)

    return 0;
}

Gdy kompilator napotka wywołanie funkcji max<int>(1, 2), ustali, że definicja funkcji dla max<int>(int, int) jeszcze nie istnieje. W rezultacie kompilator domyślnie użyje naszego max<T> szablonu funkcji do jego utworzenia.

Proces tworzenia funkcji (z określonymi typami) z szablonów funkcji (z typami szablonów) nazywa się w skrócie instancją szablonu funkcji (lub instancją w skrócie). Kiedy w wyniku wywołania funkcji tworzona jest instancja funkcji, nazywa się ją niejawną instancją. Funkcja utworzona na podstawie szablonu jest technicznie nazywana specjalizacją, ale w języku potocznym często nazywana jest instancją funkcji. Szablon, na podstawie którego tworzona jest specjalizacja, nazywany jest szablonem podstawowym. Instancje funkcji są pod każdym względem normalnymi funkcjami.

Nomenklatura

Termin „specjalizacja” jest częściej używany w odniesieniu do jawnej specjalizacji, która pozwala nam jawnie zdefiniować specjalizację (zamiast jej domyślnego tworzenia instancji na podstawie podstawowego szablonu). Wyraźną specjalizację omówimy na lekcji 26.3 -- Specjalizacja szablonu funkcji.

Proces tworzenia instancji funkcji jest prosty: kompilator zasadniczo klonuje podstawowy szablon i zastępuje typ szablonu (T) rzeczywistym typem, który określiliśmy (int).

Więc kiedy wywołujemy max<int>(1, 2), specjalizacja funkcji, która zostaje utworzona, wygląda mniej więcej tak:

template<> // ignore this for now
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
    return (x < y) ? y : x;
}

Oto ten sam przykład co powyżej, pokazujący, co kompilator faktycznie kompiluje po wykonaniu wszystkich instancji:

#include <iostream>

// a declaration for our function template (we don't need the definition any more)
template <typename T> 
T max(T x, T y);

template<>
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n'; // instantiates and calls function max<int>(int, int)

    return 0;
}

Możesz to skompilować samodzielnie i sprawdzić, czy to działa. Szablon funkcji jest tworzony tylko przy pierwszym wywołaniu funkcji w każdej jednostce tłumaczenia. Dalsze wywołania funkcji są kierowane do już utworzonej funkcji.

I odwrotnie, jeśli nie zostanie wykonane żadne wywołanie funkcji do szablonu funkcji, szablon funkcji nie zostanie utworzony w tym tłumaczeniu. jednostka.

Zróbmy inny przykład:

#include <iostream>

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

int main()
{
    std::cout << max<int>(1, 2) << '\n';    // instantiates and calls function max<int>(int, int)
    std::cout << max<int>(4, 3) << '\n';    // calls already instantiated function max<int>(int, int)
    std::cout << max<double>(1, 2) << '\n'; // instantiates and calls function max<double>(double, double)

    return 0;
}

Działa to podobnie do poprzedniego przykładu, ale tym razem nasz szablon funkcji zostanie użyty do wygenerowania dwóch funkcji: raz zastąpimy T z int, a drugi raz zastąpimy T z double. Po wszystkich instancjach program będzie wyglądał mniej więcej tak:

#include <iostream>

// a declaration for our function template (we don't need the definition any more)
template <typename T>
T max(T x, T y); 

template<>
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
    return (x < y) ? y : x;
}

template<>
double max<double>(double x, double y) // the generated function max<double>(double, double)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n';    // instantiates and calls function max<int>(int, int)
    std::cout << max<int>(4, 3) << '\n';    // calls already instantiated function max<int>(int, int)
    std::cout << max<double>(1, 2) << '\n'; // instantiates and calls function max<double>(double, double)

    return 0;
}

Dodatkowa rzecz, na którą warto zwrócić uwagę: kiedy wykonamy wszystkie instancje. instancja max<double>, utworzona funkcja ma parametry typu double Ponieważ udostępniliśmy int argumenty, argumenty te zostaną niejawnie przekonwertowane na double.

dedukcją argumentów szablonu

W większości przypadków rzeczywiste typy, których chcemy użyć do utworzenia instancji, będą odpowiadać typowi parametrów naszej funkcji. Na przykład:

std::cout << max<int>(1, 2) << '\n'; // specifying we want to call max<int>

W tym wywołaniu funkcji określiliśmy, że chcemy zamień T z int, ale wywołujemy także funkcję z int argumentami.

W przypadkach, gdy typ argumentów odpowiada rzeczywistemu typowi, który chcemy, nie musimy określać rzeczywistego typu - zamiast tego możemy użyć odliczenia argumentów szablonu , aby kompilator wydedukował rzeczywisty typ, który powinien zostać użyty z typów argumentów w funkcji zadzwoń.

Na przykład zamiast wywoływać funkcję w ten sposób:

std::cout << max<int>(1, 2) << '\n'; // specifying we want to call max<int>

Możemy zamiast tego wykonać jedną z poniższych czynności:

std::cout << max<>(1, 2) << '\n';
std::cout << max(1, 2) << '\n';

W obu przypadkach kompilator zobaczy, że nie podano rzeczywistego typu, więc spróbuje wydedukować rzeczywisty typ z argumentów funkcji, co pozwoli mu wygenerować funkcję max() , w której wszystkie parametry szablonu odpowiadają typowi podanych argumentów. W tym przykładzie kompilator wywnioskuje, że użycie szablonu funkcji max<T> z rzeczywistym typem int pozwala na utworzenie instancji funkcji max<int>(int, int), tak że typ obu parametrów funkcji (int) odpowiada typowi dostarczonych argumentów (int).

Różnica między tymi dwoma przypadkami ma związek ze sposobem, w jaki kompilator rozwiązuje wywołanie funkcji ze zbioru przeciążonych funkcji. (z pustymi nawiasami kątowymi) kompilator weźmie pod uwagę tylko max<int> przeciążenia funkcji szablonu podczas określania, którą przeciążoną funkcję wywołać. W dolnym przypadku (bez nawiasów kątowych) kompilator uwzględni zarówno max<int> przeciążenia funkcji szablonu, jak i max przeciążenia funkcji innych niż szablon. preferowane.

Kluczowa informacja

Normalna składnia wywołania funkcji będzie preferować funkcję inną niż szablon, a nie równie wykonalną funkcję utworzoną z szablonu.

Na przykład:

#include <iostream>

template <typename T>
T max(T x, T y)
{
    std::cout << "called max<int>(int, int)\n";
    return (x < y) ? y : x;
}

int max(int x, int y)
{
    std::cout << "called max(int, int)\n";
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max<int>(1, 2) << '\n'; // calls max<int>(int, int)
    std::cout << max<>(1, 2) << '\n';    // deduces max<int>(int, int) (non-template functions not considered)
    std::cout << max(1, 2) << '\n';      // calls max(int, int)

    return 0;
}

Zauważ, że składnia w dolnej części litery wygląda identycznie jak normalne wywołanie funkcji! W większości przypadków ta normalna składnia wywołania funkcji będzie tą, której używamy do wywoływania funkcji utworzonych z szablonu funkcji.

Jest kilka powodów, dla których warto to zrobić. to:

  • Składnia jest bardziej zwięzła.
  • Rzadko zdarza się, że mamy zarówno pasującą funkcję nieszablonowy, jak i szablon funkcji.
  • Jeśli mamy pasującą funkcję nieszablonową i pasujący szablon funkcji, zwykle wolimy wywołać funkcję nieszablonową.

Ten ostatni punkt może być nieoczywisty działa dla wielu typów - ale w rezultacie musi być ogólna. Funkcja niebędąca szablonem obsługuje tylko określoną kombinację typów. Może mieć implementację, która jest bardziej zoptymalizowana lub bardziej wyspecjalizowana dla tych konkretnych typów niż wersja szablonu funkcji. Na przykład:

#include <iostream>

// This function template can handle many types, so its implementation is generic
template <typename T>
void print(T x)
{
    std::cout << x; // print T however it normally prints
}

// This function only needs to consider how to print a bool, so it can specialize how it handles
// printing of a bool
void print(bool x)
{
    std::cout << std::boolalpha << x; // print bool as true or false, not 1 or 0
}

int main()
{
    print<bool>(true); // calls print<bool>(bool) -- prints 1
    std::cout << '\n';

    print<>(true);     // deduces print<bool>(bool) (non-template functions not considered) -- prints 1
    std::cout << '\n';

    print(true);       // calls print(bool) -- prints true
    std::cout << '\n';

    return 0;
}

Najlepsza praktyka

Preferuj normalną składnię wywołań funkcji podczas wywoływania funkcji utworzonej z szablonu funkcji (chyba że chcesz, aby wersja szablonu funkcji była preferowana w stosunku do odpowiadającej funkcji niebędącej szablonem).

Funkcja szablony z parametrami innymi niż szablonowe

Możliwe jest tworzenie szablonów funkcji, które mają zarówno parametry szablonu, jak i parametry inne niż szablonowe. Parametry szablonu typu można dopasować do dowolnego typu, a parametry inne niż szablonowe działają jak parametry normalnych funkcji.

Na przykład:

// T is a type template parameter
// double is a non-template parameter
// We don't need to provide names for these parameters since they aren't used
template <typename T>
int someFcn(T, double)
{
    return 5;
}

int main()
{
    someFcn(1, 3.4); // matches someFcn(int, double)
    someFcn(1, 3.4f); // matches someFcn(int, double) -- the float is promoted to a double
    someFcn(1.2, 3.4); // matches someFcn(double, double)
    someFcn(1.2f, 3.4); // matches someFcn(float, double)
    someFcn(1.2f, 3.4f); // matches someFcn(float, double) -- the float is promoted to a double

    return 0;
}

Ten szablon funkcji ma pierwszy parametr oparty na szablonie, ale drugi parametr ma ustalony typ double. Należy pamiętać, że zwracanym typem może być również dowolny typ. W tym przypadku nasz funkcja zawsze zwróci int wartości.

Funkcje utworzone w instancji mogą nie zawsze się skompilować

Rozważ następujący program:

#include <iostream>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::cout << addOne(1) << '\n';
    std::cout << addOne(2.3) << '\n';

    return 0;
}

Kompilator skutecznie skompiluje i wykona to:

#include <iostream>

template <typename T>
T addOne(T x);

template<>
int addOne<int>(int x)
{
    return x + 1;
}

template<>
double addOne<double>(double x)
{
    return x + 1;
}

int main()
{
    std::cout << addOne(1) << '\n';   // calls addOne<int>(int)
    std::cout << addOne(2.3) << '\n'; // calls addOne<double>(double)

    return 0;
}

co da wynik:

2
3.3

Ale co, jeśli spróbujemy czegoś takiego?

#include <iostream>
#include <string>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::string hello { "Hello, world!" };
    std::cout << addOne(hello) << '\n';

    return 0;
}

Kiedy kompilator próbuje rozwiązać addOne(hello) nie znajdzie dopasowania funkcji innej niż szablon for addOne(std::string), ale znajdzie nasz szablon funkcji dla addOne(T) i ustali, że może wygenerować z niego addOne(std::string) funkcję. Zatem kompilator wygeneruje i skompiluje to:

#include <iostream>
#include <string>

template <typename T>
T addOne(T x);

template<>
std::string addOne<std::string>(std::string x)
{
    return x + 1;
}

int main()
{
    std::string hello{ "Hello, world!" };
    std::cout << addOne(hello) << '\n';

    return 0;
}

Wygeneruje to jednak błąd kompilacji, ponieważ x + 1 nie ma sensu, gdy x jest std::string wywołaj addOne() z argumentem typu std::string.

Funkcje instancyjne mogą nie zawsze mieć sens semantyczny

Kompilator pomyślnie skompiluje utworzony szablon funkcji, o ile ma to sens składniowy. Jednak kompilator nie ma możliwości sprawdzenia, czy taka funkcja faktycznie ma sens semantyczny.

Na przykład:

#include <iostream>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::cout << addOne("Hello, world!") << '\n';

    return 0;
}

W tym przykładzie wywołujemy addOne() literał łańcuchowy w stylu C. Co to właściwie oznacza semantycznie? Kto wie!

Być może to zaskakujące, ponieważ syntaktycznie C++ pozwala na dodanie wartości całkowitej do literału łańcuchowego (omówimy to w przyszłej lekcji 17,9 -- Arytmetyka wskaźników i indeksy dolne), powyższy przykład kompiluje i daje następujący wynik:

ello, world!

Ostrzeżenie

Kompilator utworzy instancję i skompiluje szablony funkcji, które nie mają sensu semantycznie, o ile są poprawne pod względem składniowym. Twoim obowiązkiem jest upewnienie się, że wywołujesz takie szablony funkcji z argumentami, które mają sens.

Dla zaawansowanych czytelników

Możemy powiedzieć kompilatorowi, że tworzenie instancji szablonów funkcji z określonymi argumentami nie powinno być dozwolone. Odbywa się to za pomocą specjalizacji szablonów funkcji, która pozwala nam przeciążać szablon funkcji dla określonego zestawu argumentów szablonu, wraz z = delete, który informuje kompilator, że każde użycie funkcji powinno wygenerować błąd kompilacji.

#include <iostream>
#include <string>

template <typename T>
T addOne(T x)
{
    return x + 1;
}

// Use function template specialization to tell the compiler that addOne(const char*) should emit a compilation error
// const char* will match a string literal
template <>
const char* addOne(const char* x) = delete;

int main()
{
    std::cout << addOne("Hello, world!") << '\n'; // compile error

    return 0;
}

Specjalizację szablonów funkcji omówimy w lekcji 26.3 -- Specjalizacja szablonu funkcji.

Szablony funkcji i domyślne argumenty dla parametrów innych niż szablony

Tak jak normalne funkcje, szablony funkcji mogą mieć argumenty domyślne dla parametrów innych niż szablon. Każda funkcja utworzona z szablonu będzie używać tego samego domyślnego argumentu.

Na przykład:

#include <iostream>

template <typename T>
void print(T val, int times=1)
{
    while (times--)
    {
        std::cout << val;        
    }
}

int main()
{
    print(5);      // print 5 1 time
    print('a', 3); // print 'a' 3 times

    return 0;
}

Wypisuje:

5aaa

Uważaj na szablony funkcji z modyfikowalnymi statycznymi zmiennymi lokalnymi

W lekcji 7.11 -- Statyczne zmienne lokalne, omówiliśmy statyczne zmienne lokalne, które są zmiennymi lokalnymi o statycznym czasie trwania (utrzymują się przez cały czas istnienia programu).

Kiedy w szablonie funkcji używana jest statyczna zmienna lokalna, każda funkcja utworzona na podstawie tego szablonu będzie miała osobną wersję statycznej zmiennej lokalnej. Rzadko stanowi to problem, jeśli statyczna zmienna lokalna ma wartość stałą. Jeśli jednak statyczna zmienna lokalna jest zmodyfikowana, wyniki mogą nie być zgodne z oczekiwaniami.

Na przykład:

#include <iostream>

// Here's a function template with a static local variable that is modified
template <typename T>
void printIDAndValue(T value)
{
    static int id{ 0 };
    std::cout << ++id << ") " << value << '\n';
}

int main()
{
    printIDAndValue(12);
    printIDAndValue(13);

    printIDAndValue(14.5);

    return 0;
}

Daje to wynik:

1) 12
2) 13
1) 14.5

Być może spodziewałeś się wydrukowania ostatniej linii 3) 14.5. Jednak to właśnie kompilator faktycznie kompiluje i wykonuje:

#include <iostream>

template <typename T>
void printIDAndValue(T value);

template <>
void printIDAndValue<int>(int value)
{
    static int id{ 0 };
    std::cout << ++id << ") " << value << '\n';
}

template <>
void printIDAndValue<double>(double value)
{
    static int id{ 0 };
    std::cout << ++id << ") " << value << '\n';
}

int main()
{
    printIDAndValue(12);   // calls printIDAndValue<int>()
    printIDAndValue(13);   // calls printIDAndValue<int>()

    printIDAndValue(14.5); // calls printIDAndValue<double>()

    return 0;
}

Zauważ to printIDAndValue<int> i printIDAndValue<double> każdy ma swoją własną, niezależną statyczną zmienną lokalną o nazwie id, a nie tę, która jest między nimi współdzielona.

Programowanie ogólne

Ponieważ typy szablonów można zastąpić dowolnym rzeczywistym typem, typy szablonów są czasami nazywane typami ogólnymi. A ponieważ szablony można pisać niezależnie od określonych typów, programowanie przy użyciu szablonów jest czasami nazywane programowaniem ogólnym. Podczas gdy C++ zazwyczaj kładzie duży nacisk na typy i sprawdzanie typów, programowanie generyczne pozwala nam skupić się na logice algorytmów i projektowaniu struktur danych, nie martwiąc się tak bardzo o informacje o typach.

Wnioski

Kiedy już przyzwyczaisz się do pisania szablonów funkcji, przekonasz się, że pisanie ich nie zajmuje dużo więcej czasu niż funkcje z rzeczywistymi typami. Szablony funkcji mogą znacznie ograniczyć konserwację kodu i liczbę błędów, minimalizując ilość kodu, który należy napisać i utrzymać.

Szablony funkcyjne mają kilka wad i grzechem byłoby nie wspomnieć o nich. Najpierw kompilator utworzy (i skompiluje) funkcję dla każdego wywołania funkcji z unikalnym zestawem typów argumentów. Zatem chociaż szablony funkcji można łatwo napisać, można je rozszerzyć do ogromnej ilości kodu, co może prowadzić do rozdęcia kodu i wydłużenia czasu kompilacji. Większą wadą szablonów funkcji jest to, że mają tendencję do generowania dziwnie wyglądających, graniczących z nieczytelnością komunikatów o błędach, które są znacznie trudniejsze do rozszyfrowania niż te w przypadku zwykłych funkcji. Te komunikaty o błędach mogą być dość onieśmielające, ale kiedy już zrozumiesz, co chcą Ci powiedzieć, wskazywane przez nie problemy są często dość proste do rozwiązania.

Te wady są dość niewielkie w porównaniu z wydajnością i bezpieczeństwem, jakie szablony wnoszą do zestawu narzędzi programistycznych, więc używaj szablonów swobodnie wszędzie tam, gdzie potrzebujesz elastyczności pisania! Dobrą zasadą jest utworzenie najpierw normalnych funkcji, a następnie przekształcenie ich w szablony funkcji, jeśli okaże się, że potrzebujesz przeciążenia dla różnych typów parametrów.

Najlepsza praktyka

Użyj szablonów funkcji, aby napisać ogólny kod, który będzie mógł pracować z szeroką gamą typów, kiedy tylko zajdzie taka potrzeba.

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:  
176 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze