11.8 -- Szablony funkcji z wieloma typami szablonów

W lekcji 11.6 -- Szablony funkcji, napisaliśmy szablon funkcji do obliczenia maksimum dwóch wartości:

#include <iostream>

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

int main()
{
    std::cout << max(1, 2) << '\n';   // will instantiate max(int, int)
    std::cout << max(1.5, 2.5) << '\n'; // will instantiate max(double, double)

    return 0;
}

Rozważmy teraz następujące podobne program:

#include <iostream>

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

int main()
{
    std::cout << max(2, 3.5) << '\n';  // compile error

    return 0;
}

Możesz być zaskoczony, gdy odkryjesz, że ten program się nie skompiluje. Zamiast tego kompilator wyświetli kilka (prawdopodobnie wyglądających na szaleńczo) komunikatów o błędach. W Visual Studio autor otrzymał następującą informację:

Project3.cpp(11,18): error C2672: 'max': no matching overloaded function found
Project3.cpp(11,28): error C2782: 'T max(T,T)': template parameter 'T' is ambiguous
Project3.cpp(4): message : see declaration of 'max'
Project3.cpp(11,28): message : could be 'double'
Project3.cpp(11,28): message : or       'int'
Project3.cpp(11,28): error C2784: 'T max(T,T)': could not deduce template argument for 'T' from 'double'
Project3.cpp(4): message : see declaration of 'max'

W naszym wywołaniu funkcji max(2, 3.5) przekazujemy argumenty dwóch różnych typów: jeden int i jeden double. Ponieważ wykonujemy wywołanie funkcji bez użycia nawiasów ostrych do określenia rzeczywistego typu, kompilator najpierw sprawdzi, czy istnieje dopasowanie inne niż szablonowe dla max(int, double). Nie znajdzie żadnego.

Następnie kompilator sprawdzi, czy uda mu się znaleźć dopasowanie do szablonu funkcji (używając dedukcji argumentów szablonu, co omówiliśmy w lekcji 11.7 -- Szablon funkcji instancja). Jednak to również się nie powiedzie z prostego powodu: T może reprezentować tylko jeden typ. Nie ma typu T , który umożliwiłby kompilatorowi utworzenie instancji szablonu funkcji max<T>(T, T) w funkcji z dwoma różnymi typami parametrów. Inaczej mówiąc, ponieważ oba parametry w szablonie funkcji są typu T, muszą zostać rozstrzygnięte na ten sam rzeczywisty typ.

Ponieważ nie można znaleźć dopasowania innego niż szablon i dopasowania szablonu, wywołanie funkcji nie zostanie rozwiązane i otrzymamy błąd kompilacji.

Możesz się zastanawiać, dlaczego kompilator nie wygenerował funkcji max<double>(double, double) , a następnie użyj konwersji numerycznej, aby wpisać konwertuj argument int do double. Odpowiedź jest prosta: konwersja typów jest wykonywana tylko podczas rozwiązywania przeciążeń funkcji, a nie podczas dedukowania argumentów szablonu.

Ten brak konwersji typów jest zamierzony z co najmniej dwóch powodów. Po pierwsze, pomaga to zachować prostotę: albo znajdziemy dokładne dopasowanie między argumentami wywołania funkcji i parametrami typu szablonu, albo nie. Po drugie, pozwala nam tworzyć szablony funkcji dla przypadków, w których chcemy mieć pewność, że dwa lub więcej parametrów będzie tego samego typu (jak w powyższym przykładzie).

Będziemy musieli znaleźć inne rozwiązanie. Na szczęście możemy rozwiązać ten problem na (przynajmniej) trzy sposoby.

Użyj static_cast, aby przekonwertować argumenty na pasujące typy

Pierwsze rozwiązanie polega na nałożeniu na osobę wywołującą ciężaru konwersji argumentów na pasujące typy. Na przykład:

#include <iostream>

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

int main()
{
    std::cout << max(static_cast<double>(2), 3.5) << '\n'; // convert our int to a double so we can call max(double, double)

    return 0;
}

Teraz, gdy oba argumenty są typu double, kompilator będzie mógł utworzyć instancję max(double, double) która spełni wymagania tego wywołania funkcji.

To rozwiązanie jest jednak niewygodne i trudne do odczytania.

Podaj jawny argument szablonu typu

Gdybyśmy napisali niebędącą szablonem max(double, double) , wtedy moglibyśmy wywołać max(int, double) i pozwolić, aby niejawne reguły konwersji typu przekonwertowały nasz int argument na double , aby wywołanie funkcji mogło zostać rozwiązane:

#include <iostream>

double max(double x, double y)
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(2, 3.5) << '\n'; // the int argument will be converted to a double

    return 0;
}

Jednak gdy kompilator dokonuje dedukcji argumentów szablonu, nie dokona żadnej konwersji typów. Na szczęście nie musimy korzystać z dedukcji argumentów szablonu, jeśli zamiast tego określimy jawny argument szablonu typu:

#include <iostream>

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

int main()
{
    // we've explicitly specified type double, so the compiler won't use template argument deduction
    std::cout << max<double>(2, 3.5) << '\n';

    return 0;
}

W powyższym przykładzie wywołujemy max<double>(2, 3.5). Ponieważ wyraźnie określiliśmy, że T należy zastąpić double, kompilator nie będzie korzystał z dedukcji argumentów szablonu. Zamiast tego po prostu utworzy instancję funkcji max<double>(double, double), a następnie napisze konwertuj wszelkie niedopasowane argumenty. Nasz int parametr zostanie niejawnie przekonwertowany na double.

Chociaż jest to bardziej czytelne niż użycie static_cast, byłoby jeszcze ładniej, gdybyśmy w ogóle nie musieli myśleć o typach podczas wywoływania funkcji max .

Szablony funkcji z wieloma parametrami typu szablonu

Źródłem naszego problemu jest to, że zdefiniowaliśmy tylko jeden typ szablonu (T) dla naszego szablonu funkcji, a następnie określono, że oba parametry muszą być tego samego typu.

Najlepszym sposobem rozwiązania tego problemu jest przepisanie naszego szablonu funkcji w taki sposób, aby nasze parametry mogły zostać rozdzielone na różne typy. Zamiast używać jednego parametru typu szablonu T, użyjemy teraz dwóch (T i U):

#include <iostream>

template <typename T, typename U> // We're using two template type parameters named T and U
T max(T x, U y) // x can resolve to type T, and y can resolve to type U
{
    return (x < y) ? y : x; // uh oh, we have a narrowing conversion problem here
}

int main()
{
    std::cout << max(2, 3.5) << '\n'; // resolves to max<int, double>

    return 0;
}

Ponieważ zdefiniowaliśmy x z typem szablonu T, I y z typem szablonu U, x i y mogą teraz niezależnie rozstrzygać ich typy. Kiedy zadzwonimy max(2, 3.5), T może być int i U może być double. Kompilator z radością utworzy instancję max<int, double>(int, double) dla nas.

Kluczowa informacja

Ponieważ T i U są niezależnymi parametrami szablonu, rozwiązują ich typy niezależnie od siebie. To oznacza T i U mogą być rozpoznawane na różne typy lub mogą być rozpoznawane na ten sam typ.

Jednak ten przykład nie działa poprawnie. Jeśli skompilujesz i uruchomisz program (z wyłączoną opcją „traktuj ostrzeżenia jako błędy”), wyświetli się następujący wynik:

3

Co się tutaj dzieje? Jak można max 2 i 3.5 Być 3?

Operator warunkowy (?:) wymaga, aby jego operandy (niewarunkowe) były tego samego wspólnego typu. Zwykłe reguły arytmetyczne (10.5 — Konwersje arytmetyczne) służą do określenia, jaki będzie wspólny typ, a wynik operatora warunkowego również będzie korzystał z tego wspólnego typu. Na przykład powszechny typ int i double Jest double, więc gdy operandami (bezwarunkowymi) naszego operatora warunkowego są an int oraz a double, wartość wygenerowana przez operator warunkowy będzie typu double. W tym przypadku to jest wartość 3.5, co jest prawidłowe.

Jednak zadeklarowanym typem zwracanym przez naszą funkcję jest T. Podczas oceny T jest int i U jest double, typem zwracanym przez funkcję jest int. Nasza wartość 3.5 przechodzi zawężającą konwersję do int wartości 3, co powoduje utratę danych (i prawdopodobnie ostrzeżenie kompilatora).

Jak więc to rozwiązać? Dokonywanie zwrotu typu a U zamiast tego nie rozwiązuje problemu, ponieważ max(3.5, 2) has U jako int i będzie prezentował ten sam problem.

W takich przypadkach odliczenie typu zwrotu (via auto) może się przydać — pozwolimy kompilatorowi wydedukować, jaki powinien być typ zwracany na podstawie instrukcji return:

#include <iostream>

template <typename T, typename U>
auto max(T x, U y) // ask compiler can figure out what the relevant return type is
{
    return (x < y) ? y : x;
}

int main()
{
    std::cout << max(2, 3.5) << '\n';

    return 0;
}

Ta wersja max teraz działa poprawnie z operandami różnych typów. Pamiętaj tylko, że funkcja z auto typ zwracany musi być w pełni zdefiniowany, zanim będzie można go użyć (deklaracja forward nie wystarczy), ponieważ kompilator musi sprawdzić implementację funkcji, aby określić typ zwracany.

Dla zaawansowanych czytelników

Jeśli potrzebujemy funkcji, którą można zadeklarować w przód, musimy wyraźnie określić typ zwracany. Ponieważ nasz typ zwrotu musi być powszechnym typem T i U, możemy skorzystać std::common_type_t (omówione na lekcji 10.5 — Konwersje arytmetyczne), aby pobrać typowy typ T i U do użycia jako nasz jawny typ zwrotu:

#include <iostream>
#include <type_traits> // for std::common_type_t

template <typename T, typename U>
auto max(T x, U y) -> std::common_type_t<T, U>; // returns the common type of T and U

int main()
{
    std::cout << max(2, 3.5) << '\n';

    return 0;
}

template <typename T, typename U>
auto max(T x, U y) -> std::common_type_t<T, U>
{
    return (x < y) ? y : x;
}

Skrócone szablony funkcji C++20

C++20 wprowadza nowe zastosowanie metody auto słowo kluczowe: Kiedy auto słowo kluczowe jest używane jako typ parametru w normalnej funkcji, kompilator automatycznie przekonwertuje tę funkcję na szablon funkcji, przy czym każdy parametr auto stanie się niezależnym parametrem typu szablonu. Ta metoda tworzenia szablonu funkcji nazywa się skróconym szablonem funkcji.

Na przykład:

auto max(auto x, auto y)
{
    return (x < y) ? y : x;
}

jest skrótem w C++ 20 dla następujących elementów:

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

co jest tym samym co max szablon funkcji, który napisaliśmy powyżej.

W przypadkach, gdy chcesz, aby każdy parametr typu szablonu był niezależnym typem, preferowana jest ta forma, ponieważ usunięcie linii deklaracji parametrów szablonu sprawi, że kod będzie bardziej zwięzły i czytelny.

Nie ma zwięzłego sposobu użycia skróconych szablonów funkcji, jeśli chcesz, aby więcej niż jeden parametr auto był tego samego typu. Oznacza to, że nie ma prostego, skróconego szablonu funkcji dla czegoś takiego:

template <typename T>
T max(T x, T y) // two parameters of the same type
{
    return (x < y) ? y : x;
}

Najlepsza praktyka

Możesz używać skróconych szablonów funkcji z pojedynczym parametrem auto lub tam, gdzie każdy parametr auto powinien być typu niezależnego (a standard języka jest ustawiony na C++ 20 lub nowszy).

Szablony funkcji mogą być przeciążone

Podobnie jak funkcje mogą być przeciążone, szablony funkcji mogą być również przeciążone. Takie przeciążenia mogą mieć różną liczbę typów szablonów i/lub inną liczbę lub typ parametrów funkcji:

#include <iostream>

// Add two values with matching types
template <typename T>
auto add(T x, T y)
{
    return x + y;
}

// Add two values with non-matching types
// As of C++20 we could also use auto add(auto x, auto y)
template <typename T, typename U>
auto add(T x, U y)
{
    return x + y;
}

// Add three values with any type
// As of C++20 we could also use auto add(auto x, auto y, auto z)
template <typename T, typename U, typename V>
auto add(T x, U y, V z)
{
    return x + y + z;
}

int main()
{
    std::cout << add(1.2, 3.4) << '\n'; // instantiates and calls add<double>()
    std::cout << add(5.6, 7) << '\n';   // instantiates and calls add<double, int>()
    std::cout << add(8, 9, 10) << '\n'; // instantiates and calls add<int, int, int>()

    return 0;
}

Jedną interesującą uwagą jest to, że w przypadku wywołania add(1.2, 3.4) kompilator będzie preferował add<T>(T, T) za add<T, U>(T, U) nawet jeśli oba mogą pasować.

Zasady określania, który z wielu pasujących szablonów funkcji powinien być preferowany, nazywane są „częściowym porządkowaniem szablonów funkcji”. Krótko mówiąc, preferowany będzie dowolny szablon funkcji, który jest bardziej restrykcyjny/specjalistyczny. add<T>(T, T) jest bardziej restrykcyjnym szablonem funkcji w tym przypadku (ponieważ ma tylko jeden parametr szablonu), więc jest preferowany.

Jeśli wiele szablonów funkcji może dopasować wywołanie, a kompilator nie może określić, który jest bardziej restrykcyjny, kompilator wyświetli błąd z niejednoznacznym dopasowaniem.

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