Argumenty szablonu są wnioskowane dla naszej wygody, żeby nie trzeba było ich jawnie podawać i nie pomylić się przy okazji. Kompilator wnioskuje argumenty szablonu w następujących przypadkach:
wywołanie nieskładowej funkcji szablonowej (najczęstsze i najprostsze),
wywołanie składowej funkcji szablonowej (w tym konstruktora),
inicjalizacja zmiennej typu auto
(w tym parametrów funkcji i
zwracanych wartości).
Wnioskowanie omówimy na przykładzie nieskładowej funkcji szablonowej. Dla wywołania funkcji szablonowej, kompilator musi skonkretyzować szablon funkcji, czyli wygenerować kod funkcji szablonowej na podstawie szablonu funkcji i jego argumentów.
Kompilator wnioskuje argumenty szablonu (funkcji) na podstawie:
typów parametrów funkcji,
typów i kategorii argumentów wywołania funkcji.
Wpływ kategorii argumentu na wnioskowanie jest dosyć skomplikowany i opisany w oddzielnym temacie doskonałego przekazywania argumentów.
W najprostszym przypadku wywołujemy funkcję z jednym parametrem:
template <parameter list>
void
foo(ParameterType t)
{
// Body of a function template.
}
int
main()
{
foo(expr);
}
Kompilator ma wywnioskować argumenty dla parametrów (tych z parameter
list
) szablonu funkcji foo
na podstawie wyrażenia expr
i typu
ParameterType
parametru t
funkcji foo
. Żeby można mówić o
wnioskowaniu, typ ParameterType
musi zależeć od (użyć w swojej
definicji) co najmniej jednego parametru szablonu. Sposobów
zdefiniowania typu ParameterType
w zależności od parametrów szablonu
jest wiele, a my omówimy najważniejsze.
Zasada: wywnioskowany argument szablonu ma pozwolić na inicjalizację parametru funkcji.
Inicjalizacja zawsze odbywa się na podstawie argumentu (jawnego bądź domyślnego) funkcji.
Ograniczenie: inicjalizacja ma się odbyć bez konwersji typu.
Mowa tu o konwersji typu argumentu expr
wywołania funkcji do typu
ParameterType
parametru funkcji. Przy wywołaniu zwykłej
(nieszablonowej) funkcji tego ograniczenia nie ma (tam konwersje są
dozwolone).
Inicjalizacja parametru t
w powyższym najprostszym przypadku wygląda
zatem tak:
ParameterType t = expr;
Kompilator musi tak wywnioskować argumenty szablonu, których parametry
są użyte w definicji typu ParameterType
, żeby inicjalizacja była
możliwa bez konwersji typu. Może się to okazać niemożliwe, co
uniemożliwia konkretyzację szablonu.
Na przykład, jeżeli ParameterType
jest typem referencyjnym na obiekt
stały typu T
, gdzie T
jest parametrem szablonu, a argumentem
wywołania funkcji jest 1
, to inicjalizacja wygląda tak:
const T &t = 1;
Wywnioskowanym argumentem będzie T = int
, bo wtedy ta inicjalizacja
jest możliwa bez konwersji typu. Jeżeli jednak ParameterType
jest
typem referencyjnym na obiekt niestały typu T
, to inicjalizacja
wygląda tak:
T &t = 1;
Wywnioskowanym argumentem będzie ciągle T = int
, bo r-wartość typu
wbudowanego (literał 1
) jest typu niestałego (tak powiada standard).
Zatem konkretyzacja nie powiedzie się, bo l-referencja niestała t
nie może być zainicjalizowana r-wartością.
Coś jest nie tak z tym brakiem konwersji w przykładach wyżej:
ParameterType
jest const int &
, a wyrażenie 1
jest typu
int
! Gdzie tu zgodność? To teraz precyzyjniej: typ
ParameterType
parametru funkcji i typ argumentu expr
mogą się
różnić wyłącznie kwalifikatorami i deklaratorem &
najwyższego rzędu,
zgodnie z zasadami (opisanymi niżej) inicjalizowania zmiennych.
Wywnioskowany argument szablonu jest typem argumentu funkcji z tymi
ewentualnymi różnicami.
Miejsce użycia kwalifikatora i deklaratora w definiowanym typie ma wpływ na:
inicjalizację wartości tego typu,
przeciążenie funkcji pod względem parametru tego typu.
Kwalifikatory i deklaratory typu mogą być najwyższego rzędu.
Deklaratory *
typu wskaźnikowego i &
typu referencyjnego mogą
znaleźć się w różnych miejscach w definiowanym typie. Deklaratorem
najwyższego rzędu jest ten pierwszy od prawej strony. Na przykład,
w typie int * const &
deklaratorem najwyższego rzędu jest &
.
Kwalifikatory typu (const
i volatile
) najwyższego rzędu mogą
występować w każdym typie za wyjątkiem typu referencyjnego.
Definiowany typ zwykły (niewskaźnikowy i niereferencyjny) może mieć
kwalifikatory typu najwyższego rzędu podane przed albo po nazwie
używanego typu, np. const int
albo int const
(i są to te same
typy). Kwalifikatory te mają znaczenie wyłącznie na etapie kompilacji
(nie uruchomienia): kompilator nie może pozwolić modyfikować danej
typu const
, a dostępu do danej typu volatile
nie może
optymalizować.
Swoboda. Zmienną możemy inicjalizować wyrażeniem inicjalizującym, nawet jeżeli ich typy (zwykłe) różnią się jedynie kwalifikatorami najwyższego rzędu, bo chodzi tylko o kopiowanie wartości. Przykład:
#include <concepts>
int main()
{
static_assert(std::same_as<const int, int const>);
int a1 = 1;
const int a2 = 2;
volatile int a3 = 3;
const volatile int a4 = 4;
int b1 = a4;
const int b2 = a3;
volatile int b3 = a2;
const volatile int b4 = a1;
}
Ta swoboda dotycz także inicjalizacji parametru funkcji z użyciem argumentu funkcji, z czego wynika ograniczenie.
Ograniczenie. Nie można przeciążać funkcji pod względem kwalifikatorów typu zwykłego (parametru funkcji), ponieważ inicjalizacja parametru typu zwykłego (gdzie swoboda nie stawia warunków dotyczących kwalifikatorów) nie jest w stanie wpłynąć na wybór przeciążenia.
Te kwalifikatory (które są szczegółem implementacji ciała funkcji) i
tak nie mają znaczenia dla strony wywołującej funkcję i dlatego są
usuwane przez kompilator z sygnatury funkcji (nie są one częścią
interfejsu funkcji), żeby umożliwić konsolidację. Proszę sprawdzić
(komendą nm
) sygnatury funkcji w tablicy symboli programu poniżej.
void foo(int)
{
}
// This function has same signature as the one above even though their
// parameters differ with the qualifiers.
// void foo(const int)
// {
// }
int main()
{
const int i = 1;
foo(i);
// Can point to a function with a regular parameter that is either
// non-const or const.
void (*fp1)(int) = foo;
void (*fp2)(const int) = foo;
}
Kwalifikatory typu wskaźnikowego znajdują się na prawo od deklaratora
*
najwyższego rzędu, czyli na końcu definicji typu (albo na początku
patrząc od prawej strony). Odnoszą się one do zmiennej wskaźnikowej,
a nie do wskazywanych danych.
Tak jak w przypadku zmiennej zwykłego typu, zmienną wskaźnikową możemy inicjalizować na podstawie wartości wskaźnika, nawet jeżeli ich typy (wskaźnikowe) różnią się kwalifikatorami najwyższego rzędu, bo wartość wskaźnika jest jedynie kopiowana i dlatego też nie możemy przeciążać funkcji dla typów wskaźnikowych (parametrów funkcji) różniących się kwalifikatorami najwyższego rzędu. Przykład:
void foo(int *)
{
}
// This function has same signature as the one above.
// void foo(int * const)
// {
// }
int main()
{
int i = 1;
int * const p1 = &i;
int * p2 = p1;
foo(p1);
foo(p2);
}
W typie wskaźnikowym, tuż po lewej stronie deklaratora *
najwyższego
rzędu, możemy też podać kwalifikatory najwyższego rzędu dla typu
wskazywanych danych. Kwalifikatory te możemy nazwać niższego
rzędu, jeżeli mówimy o typie wskaźnikowym.
Warunek dla typu wskaźnikowego. Kwalifikatory niższego rzędu typu wskaźnikowego muszą zawierać kwalifikatory najwyższego rzędu typu wskazywanych danych.
Oto przykład:
int main()
{
int a1 = 1;
const int a2 = 2;
volatile int a3 = 3;
const volatile int a4 = 4;
// The commented lines below would bypass the qualifiers.
int * p11 = &a1;
// int * p12 = &a2;
// int * p13 = &a3;
// int * p14 = &a4;
const int * p21 = &a1;
const int * p22 = &a2;
// const int * p23 = &a3;
// const int * p24 = &a4;
volatile int * p31 = &a1;
// volatile int * p32 = &a2;
volatile int * p33 = &a3;
// volatile int * p34 = &a4;
const volatile int * p41 = &a1;
const volatile int * p42 = &a2;
const volatile int * p43 = &a3;
const volatile int * p44 = &a4;
}
Ten warunek pozwala przeciążać funkcje dla typów wskaźnikowych (parametru funkcji), które różnią się kwalifikatorami dla typu wskazywanych danych. Chodzi o możliwość przeciążenia pod względem typu danych, do których odnosi się wskaźnik. Oto przykład:
#include <iostream>
#include <utility>
void foo(int *)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void foo(const int *)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int main()
{
int i = 1;
foo(&i);
foo(&std::as_const(i));
}
Referencja albo nazywa pewną daną (np. element tablicy) albo jest
aliasem innej zmiennej. Typ referencyjny nie ma kwalifikatorów
najwyższego rzędu, bo referencja ma wiernie odwzorować typ danej,
której jest aliasem: wyrażenie referencyjne jest zamieniane na
wyrażenie z tą daną, do której referencja się odnosi. Co więcej,
referencję można tylko zainicjalizować, a potem już nie można jej
zmienić, żeby była aliasem innej danej, a kwalifikator const
najwyższego rzędu nie robiłby różnicy. Tak więc, na przykład, typ
int & const
jest niedopuszczalny.
Dlatego nie da się przeciążać funkcji dla typów referencyjnych (parametru funkcji), które miałyby się różnić tylko kwalifikatorami najwyższego rzędu. Nawet deklaracja jednej funkcji jest niemożliwa, jeżeli typ referencyjny jej parametru miałby mieć kwalifikator.
Podobnie jak w typie wskaźnikowym, tak i w typie referencyjnym, tuż po
lewej stronie deklaratora &
najwyższego rzędu, możemy też podać
kwalifikatory najwyższego rzędu dla typu wskazywanych danych.
Kwalifikatory te możemy nazwać niższego rzędu, jeżeli mówimy o
typie referencyjnym.
Warunek dla typu referencyjnego. Kwalifikatory niższego rzędu typu referencyjnego muszą zawierać kwalifikatory najwyższego rzędu typu wskazywanych danych.
Oto przykład:
int main()
{
int a1 = 1;
const int a2 = 2;
volatile int a3 = 3;
const volatile int a4 = 4;
// The commented lines below would bypass the qualifiers.
int & p11 = a1;
// int & p12 = a2;
// int & p13 = a3;
// int & p14 = a4;
const int & p21 = a1;
const int & p22 = a2;
// const int & p23 = a3;
// const int & p24 = a4;
volatile int & p31 = a1;
// volatile int & p32 = a2;
volatile int & p33 = a3;
// volatile int & p34 = a4;
const volatile int & p41 = a1;
const volatile int & p42 = a2;
const volatile int & p43 = a3;
const volatile int & p44 = a4;
}
Ten warunek pozwala przeciążać funkcje dla typów referencyjnych (parametru funkcji), które różnią się kwalifikatorami dla typu wskazywanych danych. Chodzi o możliwość przeciążenia pod względem typu danych, do których odnosi się referencja. Oto przykład:
#include <iostream>
#include <utility>
void foo(int &)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void foo(const int &)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int main()
{
int i = 1;
foo(i);
foo(std::as_const(i));
}
Każde wyrażenie w C++ jest typu niereferencyjnego, a więc i typ
argumentu funkcji jest niereferencyjny, nawet jeżeli wyrażeniem jest
nazwa referencji. W standardzie napisano ([expr.type]), że kompilator
usuwa deklarator &
najwyższego rzędu z typu wyrażenia przed jego
dalszym opracowaniem.
Dla każdego rodzaju parametru szablonu, kompilator może wnioskować argument. Najczęściej chcemy, żeby kompilator wnioskował typowe (w tym szablonowe) argumenty.
Typowy argument szablonu jest wnioskowany dla typowego parametru szablonu. To wnioskowanie jest najbardziej złożone (w porównaniu do rodzaju wartościowego czy szablonowego), bo rozróżnia:
typ parametru funkcji, w szczególności:
zwykły (niewskaźnikowy i niereferencyjny) dla przekazywania przez wartość,
wskaźnikowy dla przekazywania przez wskaźnik (które jest po prostu przekazywaniem przez wartość),
referencyjny dla przekazywania przez referencję.
typ argumentu wywołania funkcji, w szczególności:
funkcyjny,
tablicowy.
kategorię argumentu (opisane w innym temacie).
W zależności od omawianych szczególnych (najważniejszych) typów parametru funkcji (zwykłych, wskaźnikowych, referencyjnych) stosowane są nieco różne zasady, które jednak wynikają z podstawowej zasady z ograniczeniem. Dla tych szczególnych typów, wnioskowany typ może różnić się od typu argumentu wywołania funkcji wyłącznie kwalifikatorami lub deklaratorami najwyższego rzędu. Wnioskowanym typem nigdy nie będzie typ referencyjny, bo argument funkcji nigdy nie jest typu referencyjnego.
Wywnioskowany argument szablonu jest typem argumentu funkcji z pominięciem kwalifikatorów najwyższego rzędu.
Chodzi o to, że inicjalizacja parametrów funkcji (przy przekazywaniu argumentów wywołania przez wartość) kopiuje wartość argumentu wywołania do parametru funkcji. Wnioskowanie nie musi zadbać o kwalifikatory typu, bo w ciele funkcji działamy na kopii. W definicji parametru możemy podać kwalifikatory, żeby poprosić kompilator o pilnowanie się z tym parametrem.
Przykład:
#include <iostream>
template <typename T>
void
foo(T t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
// We cannot overload templates with parameters of regular types that
// differ with qualifiers.
// template <typename T>
// void
// foo(const T t)
// {
// }
int
main()
{
int w = 1;
const int x = 2;
volatile int y = 3;
const volatile int z = 4;
foo(w);
foo(x);
foo(y);
foo(z);
}
Ten typ paramatru szablonu może także przyjąc argument typu wskaźnikowego:
#include <iostream>
template <typename T>
void
foo(const T t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int i = 1;
int const volatile * p1 = &i;
int volatile * const p2 = &i;
int const * volatile p3 = &i;
int * const volatile p4 = &i;
foo(p1);
foo(p2);
foo(p3);
foo(p4);
}
Wywnioskowany argument szablonu jest typem argumentu funkcji z
pominięciem deklaratora *
i kwalifikatorów najwyższego rzędu.
Kwalifikatory najwyższego rzędu dla typu wskazywanych danych też są
pomijane, jeżeli znajdują się w definicji typu parametru funkcji.
Wyjaśnienie:
Deklarator *
najwyższego rzędu jest pomijany, bo on już jest w
definicji typu parametru funkcji.
Kwalifikatory najwyższego rzędu są pomijane, bo nie mają one znaczenia dla funkcji, która działa na kopii wartości argumentu przekazanego do niej (tak jak w przypadku zwykłego typu parametru funkcji).
Jeżeli w definicji typu parametru funkcji podamy kwalifikatory najwyższego rzędu dla typu wskazywanych danych, to te kwalifikatory będą pominięte we wywnioskowanym typie. Jeżeli nie zostały podane, to kwalifikatory (typu wskazywanych danych) zdefiniowane przez typ argumentu funkcji będą propagowane do wywnioskowanego typu, co pozwala na inicjalizację parametru funkcji.
#include <iostream>
template <typename T>
void
foo(T *t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
// Can't overload for pointer types of different top-level qualifiers.
// template <typename T>
// void
// foo(T * const t)
// {
// std::cout << __PRETTY_FUNCTION__ << std::endl;
// }
template <typename T>
void
foo(T volatile *t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int i = 1;
int const volatile * p1 = &i;
int volatile * const p2 = &i;
int const * volatile p3 = &i;
int * const volatile p4 = &i;
foo(p1);
foo(p2);
foo(p3);
foo(p4);
}
Wywnioskowany argument szablonu jest typem argumentu funkcji z pominięciem tych kwalifikatorów najwyższego rzędu, które zostały podane w definicji typu danych, do których referencyjny parametr funkcji się odnosi.
Chodzi o to, żeby referencyjny parametr funkcji rzeczywiście mógł być zainicjalizowany: jeżeli typ argumentu wywołania jest stały (bądź ulotny), to referencja musi odnosić się do danej typu stałego (bądź ulotnego). Pamiętajmy, że typ argumentu nigdy nie jest referencyjny.
Przykład:
#include <iostream>
template <typename T>
void
foo(T &t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
// Can't overload for reference types of different top-level
// qualifiers because no such thing exists!
// template <typename T>
// void
// foo(T & const t)
// {
// std::cout << __PRETTY_FUNCTION__ << std::endl;
// }
template <typename T>
void
foo(T volatile &t)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int w = 1;
const int x = 2;
volatile int y = 3;
const volatile int z = 4;
foo(w);
foo(x);
foo(y);
foo(z);
}
Funkcję możemy przekazać przez:
referencję,
wskaźnik,
ale nie wartość.
Funkcję możemy przekazać przez referencję używając referencyjnego
typu parametru funkcji, a dokładnie typu F &
, gdzie F
jest typowym
parametrem szablonu. Wywnioskowanym argumentem szablonu będzie typ
referencji na przekazywaną funkcję. Przykład:
#include <iostream>
void
hello()
{
std::cout << "Hello World!\n";
}
template <typename F>
void
foo(F &f)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
f();
}
void
goo(void (&f)())
{
f();
}
int
main()
{
foo(hello);
goo(hello);
// An expression of a pointer type is an rvalue, and so &hello is an
// rvalue. Since the hello function is of a non-const type (there
// is no such thing as a const non-member function), the constness
// cannot be deduced, and therefore the non-const reference
// parameter "F &" cannot be initialized with an rvalue. It would
// compile if the foo parameter was a const reference.
foo(*hello);
}
Funkcję możemy przekazać przez wskaźnik używając wskaźnikowego typu
parametru funkcji szablonowej, a dokładnie typu F *
, gdzie F
jest
typowym parametrem szablonu. Wywnioskowanym argumentem szablonu
będzie typ przekazywanej funkcji. Przykład:
#include <iostream>
void
hello()
{
std::cout << "Hello World!\n";
}
template <typename F>
void
foo(F *f)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
f();
}
void
goo(void (*f)())
{
f();
}
int
main()
{
foo(&hello);
goo(&hello);
// The following has the same effect as the above, because the
// function name decays (is converted) to the function pointer.
foo(hello);
goo(hello);
}
Zamianę nazwy funkcji na wskaźnik do niej nazywamy rozpadem funkcji na wskaźnik (ang. decay), który pochodzi z języka C. Z rozpadu skorzystaliśmy wyżej podając nazwę funkcji jako argument wywołania funkcji szablonowej.
Funkcję możemy także przekazać przez wskaźnik używając zwykłego
(niereferencyjnego i niewskaźnikowego) typu parametru funkcji, a
dokładnie typu F
, gdzie F
jest typowym parametrem szablonu. Wtedy
wywnioskowanym argumentem szablonu będzie typ wskaźnikowy na funkcję.
Przykład:
#include <iostream>
void
hello()
{
std::cout << "Hello World!\n";
}
template <typename F>
void
foo(F f)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
f();
}
void
goo(void (*f)())
{
f();
}
int
main()
{
foo(&hello);
goo(&hello);
// The following has the same effect as the above, because the
// function name decays (is converted) to the function pointer.
foo(hello);
goo(hello);
}
Tablicę języka C możemy przekazać do funkcji szablonowej przez:
referencję,
wskaźnik,
ale nie wartość.
Tablicę możemy przekazać przez referencję używając referencyjnego
typu parametru funkcji szablonowej, a dokładnie typu A &
, gdzie A
jest typowym parametrem szablonu. Wywnioskowanym argumentem będzie
typ tablicy. Przykład:
#include <iostream>
template <typename A>
void
foo(A &a)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void
goo(int (&a)[3])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int a[] = {1, 2, 3};
foo(a);
goo(a);
}
Tablicę możemy przekazać przez wskaźnik używając wskaźnikowego typu
parametru funkcji szablonowej, a dokładnie typu A *
, gdzie A
jest
typowym parametrem szablonu. Wywnioskowanym argumentem będzie typ
tablicy. Przykład:
#include <iostream>
template <typename A>
void
foo(A *a)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void
goo(int (*a)[3])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int a[] = {1, 2, 3};
// foo(a);
// goo(a);
foo(&a);
goo(&a);
}
Jeżeli typem parametru funkcji szablonowej jest zwykły typ (niereferencyjny i niewskaźnikowy), a argumentem wywołania funkcji będzie nazwa tablicy, to do funkcji zostanie przekazany wskaźnik na pierwszy element tablicy (a nie wskaźnik na tablicę), bo:
tablica rozpadnie się (ang. decay) na wskaźnik na pierwszy element tablicy,
wywnioskowanym argumentem będzie typ wskaźnikowy na element tablicy.
Oto przykład z rozpadem:
#include <iostream>
template <typename A>
void
foo(A a[])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void
goo(int a[])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
int a[] = {1, 2, 3};
foo(a);
goo(a);
// foo(&a);
// goo(&a);
}
Wartościowy argument szablonu jest wnioskowany tylko na podstawie typu argumentu wywołania funkcji, z którego można ten argument wywnioskować. Częścią typu argumentu wywołania funkcji musi być pewna wartość, którą potrzebujemy, i która staje się wywnioskowanym wartościowym argumentem.
Jedynymi typami argumentu wywołania funkcji, na podstawie których możemy wywnioskować wartościowy argument szablonu, to:
typ tablicy języka C,
dowolny typ szablonowy.
Oto przykład:
#include <iostream>
template <typename T, unsigned I>
void
roo(T (&)[I])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
template <typename T, unsigned I>
void
poo(T (*)[I])
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
int
main()
{
const char *t1[] = {"Hello", "World!"};
int t2[] = {1, 2, 3, 4, 5};
roo(t1);
roo(t2);
poo(&t1);
poo(&t2);
}
Typy wartościowych parametrów obu szablonów muszą się zgadzać. Te oba szablony to:
szablon funkcji: wartościowy parametr tego szablonu ma wywnioskowany argument,
szablon typu: tego szablonowego typu jest argument wywołania funkcji.
Na przykład, wartościowy parametr I
musi mieć typ std::size_t
, bo
takiego typu jest wartościowy parametr typu szablonowego std::array
:
#include <array>
#include <iostream>
using namespace std;
template <typename T, std::size_t I>
void
foo(const array<T, I> &)
{
cout << __PRETTY_FUNCTION__ << endl;
cout << "The array has " << I << " elements.\n";
}
int
main()
{
foo(array{"Hello ", "World!"});
foo(array{1, 2, 3, 4, 5});
}
W przykładach wyżej używaliśmy tylko jednego parametru funkcji szablonowej, więc parametry szablonu mogły być użyte w co najwyżej jednej definicji typu parametru funkcji szablonowej. Parametrów funkcji szablonowej może być jednak dowolna liczba, a parametr szablonu może być użyty w dowolnej liczbie definicji typów parametrów funkcji szablonowej. Jak wtedy wnioskowane są argumenty szablonu?
Wtedy wnioskowanie argumentów szablonu odbywa się niezależnie dla każdej pary parametru funkcji i argumentu wywołania. Dla każdej pary wnioskowane są argumenty dla parametrów szablonu, które zostały użyte w definicji typu tego parametru funkcji. Jeżeli jakiś argument został wywnioskowany więcej niż raz (czyli dla różnych par), to musi on być taki sam, w przeciwnym razie wnioskowanie nie udaje się.
Podczas wnioskowania nie jest dopuszczalna konwersja typów. W przykładzie niżej wnioskowane są różne argumenty, bo nie jest dopuszczalna konwersja różnych typów argumentów wywołania funkcji. Zatem w poniższym przykładzie wnioskowanie nie udaje się:
template <typename T>
T
max_ref(const T &a, const T &b)
{
return b < a ? a : b;
}
template <typename T>
T
max_val(T a, T b)
{
return b < a ? a : b;
}
double
MAX(const double &a, const double &b)
{
return b < a ? a : b;
}
int
main()
{
// 1 is converted to .1 (which is a temporary, an rvalue), so that a
// const reference to double (the first parameter) can bind to it.
MAX(1, .1);
// The following call to a template function fails, because:
//
// * no conversion is allowed, so 1 cannot be converted to .1.
//
// * T is first deduced int, then double, so deduction fails.
// max_ref(1, .1);
// max_val(1, .1);
// No deduction takes place.
max_ref<int>(1, '1');
max_ref<double>(1, .1);
max_val<int>(1, '1');
max_val<double>(1, .1);
}
Argument szablonu może być wywnioskowany albo podany (jawnie albo domyślnie).
Wnioskowanie argumentów szablonu zależy od typu parametru funkcji i typu argumentu wywołania funkcji.
Żeby zrozumieć wnioskowanie, należy znać szczegóły dotyczące inicjalizacji zmiennych, wskaźników i referencji.
Na podstawie czego wnioskowane są argumenty szablonu?
Czy wnioskowanie uwzględnia typ wartości zwracanej przez funkcję?
Co to jest rozpad tablicy?