Szablon może być:
funkcji,
klasy, struktury, unii,
składowej klasy (funkcji składowej, pola składowego),
aliasu typu,
zmiennej,
konceptu.
Deklaracje i definicje szablonów rozpoczynają się słowem kluczowym
template
z taką składnią:
template <parameter list>
Mówimy, że szablon jest sparametryzowany, bo ma parametry, które
nazywamy parametrami szablonu. Parametr jest zdefiniowany w
liście parametrów (parameter list
powyżej) i ma nazwę, którą
następnie używamy w deklaracji czy definicji szablonu.
Kiedy używamy szablonu (np. wywołujemy funkcję zdefiniowaną
szablonem), to po jego nazwie możemy podać argumenty szablonu w
znakach <>
. Konkretyzacja szablonu podstawia argument
szablonu w miejsca wystąpienia parametru szablonu. Mówimy także, że
parametr przyjmuje argument.
Terminy parametru i argumentu szablonu są analogiczne do terminów parametru i argumentu funkcji, ale ta analogia jest jedynie powierzchowna. Inicjalizacja parametru funkcji z użyciem argumentu funkcji ma dużo szczegółów (jak na przykład konwersje typów czy inicjalizacja referencji), które nie odnoszą się do podstawienia. Podstawienie jedynie sprawdza czy argument jest poprawny, czyli że jest typem, wartością czy szablonem typu, zgodnie z rodzajem parametru. Wniosek: podstawienie to nie inicjalizacja.
Parametry szablonu są zdefiniowane w liście parametrów, gdzie są
oddzielone przecinkami. Definicja parametru ustala rodzaj i
opcjonalną nazwę parametru. Rodzaje parametrów to: typ, wartość,
szablon. Przykład poniżej ma trzy parametry: T
typowego rodzaju,
nienazwany parametr wartościowego rodzaju i C
szablonowego rodzaju.
template <typename T, int, template<typename> typename C>
Nazwijmy to prosto: typowy parametr szablonu. I typowy on jest
też dlatego, że tego rodzaju parametr jest najczęstszy. Typowy
parametr definiujemy pisząc typename T
. Słowo kluczowe typename
mówi, że chodzi o typowy parametr, a T
jest nazwą parametru. Możemy
również równoważnie napisać class T
, ale nowocześniej jest typename
T
.
Konkretyzacja może podstawić za T
dowolny konkretny typ: typ
wbudowany (np. int
), typ użytkownika (np. A
), typ szablonowy
(np. vector<int>
), a nawet void
. Konkretny, czyli nie szablon
typu. T
nie musi spełniać żadnych warunków, np. nie musi
dziedziczyć z klasy bazowej. Wymagania dotyczące typu T
wynikają z
jego użycia w definicji szablonu, czyli czy, na przykład:
tworzymy domyślną wartość typu T
, czyli T{}
,
dodajemy, używając operator+
, wartości typu T
,
wyłuskujemy, używając operator&
, wartość typu T
,
przekazujemy jakiejś funkcji, np. push_back
, wartość typu T
,
piszemy do std::ostream
wartość typu T
z użyciem operator<<
.
To jest przykład funkcji szablonowej z typowym parametrem, gdzie kompilator jest w stanie wywnioskować argument szablonu, więc nie musimy go jawnie podawać podczas wywołania funkcji:
#include <iostream>
#include <string>
using namespace std;
template <typename T>
void
print(const T &t)
{
cout << t << endl;
}
int
main()
{
print(1);
print(0.5);
print("Hello!");
print(string("Hello!"));
}
Nazwijmy to prosto: wartościowy parametr szablonu. Parametr tego
rodzaju definiujemy pisząc some_type I
, gdzie some_type
jest
typem, np. int
. Typ some_type
nie jest dowolny, tylko nieduży
zbiór typów jest dozwolony, a najczęściej używane są typy całkowite.
Podczas kompilacji za I
podstawiana jest wartość tego typu, np. 1
dla parametru szablonu zdefiniowanego jako int I
.
Przykład definicji wartościowego parametru szablonu:
template <int N>
To jest przykład szablonu funkcji z wartościowym parametrem szablonu
N
, którego argument musi być jawnie podany, bo kompilator nie jest w
stanie go wywnioskować:
#include <iostream>
template <int N>
void
print()
{
std::cout << N << std::endl;
}
int
main()
{
print<100>();
}
W przykładzie niżej mamy dwa przeciążenia szablonu funkcji
(przeciążenia, bo mają tą samą nazwę). Drugi szablon ma wartościowy
parametr szablonu N
i typowy parametr szablonu T
, którego argument
może być wywnioskowany:
#include <iostream>
template <typename T>
void print(const T &t)
{
std::cout << t << '\n';
}
template <int N, typename T>
void print(const T &t)
{
for(int n = N; n--;)
print(t);
}
int
main()
{
print("Hello!");
print<10>("World!");
}
Przykład niżej ilustruje rekurencyjny szablon funkcji, gdzie
rekurencja jest przerwana przez instrukcję warunkową czasu kompilacji
if constexpr
:
#include <iostream>
template <typename T>
void print(const T &t)
{
std::cout << t << '\n';
}
template <int N, typename T>
void print(const T &t)
{
if constexpr (N)
{
print(t);
print<N - 1>(t);
}
}
int
main()
{
print("Hello!");
print<10>(1);
}
Jednymi z dozwolonych typów wartościowych parametrów szablonu są wskaźniki i referencje na funkcje:
#include <iostream>
template <typename T>
void
debug(T a, T b)
{
std::cout << a << b;
}
void
nodebug(int a, int b)
{
}
// The version with the callback reference.
template <void (&F)(int, int) = nodebug>
void roo(int a, int b)
{
F(a, b);
}
// The version with the callback pointer.
template <void (*F)(int, int) = nodebug>
void poo(int a, int b)
{
F(a, b);
}
int
main()
{
// The nodebug function is used.
roo(1, 2);
poo(1, 2);
// The baseline code, the same as in the debug function.
std::cout << 1 << 2;
// For GCC 13.1.0 with -O1, the following line produces the same
// assembly code as the line above.
roo<debug>(1, 2);
poo<debug>(1, 2);
}
Nazwijmy to tak: szablonowy parametr szablonu. Taki parametr
przyjmuje (jako swój argument) szablon typu o wymaganym interfejsie.
Definicja szablonowego parametru T
definiuje (przez listę parametrów
parameter-list
) interfejs dopuszczalnych szablonów typu:
template <parameter-list> typename T
Tutaj używamy parametu T
:
template <template <parameter-list> typename T>
W przykładzie niżej, za szablonowy parametr C
może być podstawiony
dowolny typ szablonowy, którego pierwszy parametr jest typowy, a drugi
wartościowy.
#include <array>
#include <iostream>
using namespace std;
template <template <typename, long unsigned> typename C, typename T>
void
foo(T t)
{
cout << __PRETTY_FUNCTION__ << endl;
C<T, 1> c1 = {t};
C<T, 2> c2 = {t, t};
}
int
main()
{
foo<array>(1);
// This is cool: pointer to an instantiated function template. We
// instantiate the function right here, because we point to it.
void (*fp)(double) = foo<array, double>;
fp(.1);
}
Za __PRETTY_FUNCTION__
kompilator (Clang, GCC) podstawia nazwę
funkcji z argumentami szablonu, więc możemy sprawdzić w jaki sposób
szablon funkcji został skonkretyzowany.
Szablonowy parametr pozwala wywnioskować argumenty skonkretyzowanego typu szablonowego:
#include <array>
#include <iostream>
using namespace std;
template <template <typename, std::size_t> typename C, std::size_t I>
void
foo(const C<int, I> &c)
{
cout << __PRETTY_FUNCTION__ << endl;
cout << "Got " << I << " elements\n";
}
int
main()
{
foo(array{1, 2, 3});
}
Szablonowy parametr pozwala przerwać zależność cykliczną między typami szablonowymi:
#include <vector>
using namespace std;
// We want to implement a container of elements, where the elements
// have a reference to the container, i.e., an element knows who owns
// it. We have two element types:
//
// A - disfunctional because of the circular dependency,
//
// B - working fine with the circular dependency resolved.
// Those elements want to have a reference to the object that owns
// them. And that's a problem, because this creates a circular
// dependency.
template <typename T>
struct A
{
T &m_t;
A(T &t): m_t(t)
{
}
};
// We can break the circular dependency with the template-kind
// parameter of a template and injected class names. This is not a
// perfect solution, because we require the owning type T to be
// templated with a single argument, so other types will not be
// accepted.
template <template <typename...> typename T>
struct B
{
// "B" is an injected class name, i.e., we do not have to write
// "B<T>" and that helps us break the circular dependency.
T<B> &m_t;
B(T<B> &t): m_t(t)
{
}
};
int
main()
{
// We can't possibly define a container of elements A because of the
// circular dependency.
// vector<A<vector<A<vector<A<vector... a1;
// vector<A<vector>> a2;
// Is the initialization ill-formed or undefined-behaved? Note that
// we use uninitialized container "b" to initialize element B(b).
vector<B<vector>> b = {B(b)};
b.push_back(B(b));
// Looks like a snake eating its own tail.
b.emplace_back(b);
}
Argumenty szablonu mogą być:
wnioskowane przez kompilator (najczęściej stosowane),
jawnie podane przez programistę (czasami niezbędne),
domyślnie podane przez programistę (czasami wygodne).
Ten przykład pokazuje wyżej wymienione przypadki:
#include <iostream>
using namespace std;
template <typename T = int>
void
print(T t = {})
{
cout << __PRETTY_FUNCTION__ << endl;
cout << t << endl;
}
int
main()
{
// A template argument is deduced.
print("Hello"); // T = const char *
print(string("World!")); // T = string
print(2020); // T = int
print(.1); // T = double
// We explicitely give a template argument.
print<double>(1); // T = double
print<string>("Hello!"); // T = string
print<double>(); // T = double
// This one produces a warning, so I commented it out.
// print<int>(1.2); // T = int
// We use the default template argument (int), and a default value
// for a call argument ({}, which is 0 for int).
print();
}
Zanim przejdziemy do wnioskowania, pierwsze omówimy jawne i domyślne argumenty szablonu.
Kiedy korzystamy z kontenerów biblioteki standardowej (a każdy robił
to na pewno), możemy jawnie podać argumenty szablonu jako część nazwy
typu używając <>
, czyli składni typ<lista argumentów>
:
#include <vector>
int
main()
{
std::vector<int> v1{3, 1, 2};
// A compiler can deduce the integer type from the initializer list.
std::vector v2{3, 1, 2};
}
Wywołując funkcję, też możemy jawnie podać argumenty szablonu, jak w
przykładzie poniższej fabryki obiektów. Argument wywołania factory
przekazujemy do konstruktora obiektu, którego typ jest określony przez
parametr T
szablonu. Kompilator nie jest w stanie wywnioskować
argumentu dla T
, więc musimy go jawnie podać.
#include <iostream>
using namespace std;
struct A
{
A(int i)
{
cout << "ctor A: " << i << endl;
}
};
struct B
{
// The constructor is templated, even though the struct is not.
template <typename T>
B(T t)
{
cout << "ctor B: " << t << endl;
}
};
template <typename T, typename A>
T
factory(A a)
{
return T(a);
}
int
main()
{
// Just as for std::make_unique.
factory<A>(1);
factory<B>(1.1);
factory<B>("Hello World!");
}
Jawne podanie argumentu szablonu jest niezbędne, jeżeli kompilator nie będzie próbował go wywnioskować (bo nie ma sposobu) i argument domyślny nie został zdefiniowany.
Możemy też jawnie podać argumenty szablonu w dwóch raczej nietypowych przypadkach (które świadczą o problemach w kodzie), kiedy:
wnioskowanie kończyłoby się błędem,
chcemy innych argumentów niż te wnioskowane przez kompilator.
Kolejność argumentów szablonu ma znaczenie (dokładnie tak samo jak w przypadku argumentów funkcji), bo są one pozycyjne, czyli pozycja argumentu w liście określa parametr. Tak więc jeżeli chcemy podać drugi argument, to musimy podać też pierwszy.
Zamieńmy miejscami parametry w przykładzie z fabryką. Ponieważ teraz
argument parametru T
podajemy jako drugi, to jako pierwszy musimy
podać argument parametru A
. Przez zmianą, argument dla A
był
wnioskowany.
#include <iostream>
using namespace std;
struct A
{
A(int i)
{
cout << "ctor A: " << i << endl;
}
};
struct B
{
template <typename T>
B(T t)
{
cout << "ctor B: " << t << endl;
}
};
template <typename A, typename T>
T
factory(A a)
{
return T(a);
}
int
main()
{
factory<int, A>(1);
factory<double, B>(1.1);
factory<const char *, B>("Hello World!");
}
Parametr szablonu (każdego rodzaju) może mieć zdefiniowany domyślny argument, który będzie użyty jeżeli nie podaliśmy argumentu jawnie i jeżeli kompilator nie próbował go wywnioskować (bo nie miał na to sposobu). Jeżeli wnioskowanie zakończy się błędem (bo kompilator miał sposób i próbował, ale mu się nie udało), to domyślny argument nie będzie użyty. Domyślny argument jest opcjonalny.
Domyślny argument podajemy po nazwie parametru z użyciem =
. Oto
przykład:
#include <deque>
#include <iostream>
#include <vector>
using namespace std;
template <template <typename...> typename C = std::vector,
typename T = int, unsigned I = 10>
C<T>
container_factory()
{
cout << __PRETTY_FUNCTION__ << endl;
return C<T>(I);
}
int
main()
{
container_factory();
container_factory<std::deque>();
container_factory<std::vector, double>();
container_factory<std::deque, bool, 1>();
}
Czasami trzeba przekazać callable jakiejś funkcji, ale nie zawsze to
callable jest wymagane. Nie chcemy przekazywać wskaźnika i sprawdzać
w czasie uruchomienia, czy jest on nullptr
, albowiem niewydajne i
nieciekawe. Chcemy, żeby callable było wkompilowane, a jeżeli nie
jest wymagane, żeby nie wprowadzało narzutu. Do tego właśnie przydaje
się domyślny argument szablonu.
Rozwiązanie: typ callable jest parametrem szablonu z domyślnym
argumentem, którym jest puste callable, czyli struktura z operatorem
wywołania o pustym ciele. Musimy też podać domyślną wartość callable
(argument wywołania funkcji), czyli {}
(argument stwórz
bezargumentowo). Oto super przykład:
#include <iostream>
struct empty_callable
{
void
operator()()
{
}
};
struct it
{
void
operator()()
{
std::cout << "Done it.\n";
}
};
template <typename C = empty_callable>
void
do_sth(C c = {})
{
c();
}
int
main()
{
// Do nothing.
do_sth();
// Do it.
do_sth(it{});
}
Szablony są podstawą programowania uogólnionego w C++.
Parametry szablonów są typowe, wartościowe i szablonowe.
Szablony funkcji są najciekawsze i najbardziej złożone pośród szablonów.
Co to jest typowy parametr szablonu?
Czego może być szablon?
Argument a parametr szablonu.