cpp

Szablony

Szablon może być:

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

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>

Rodzaj parametru: typ

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:

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

Rodzaj parametru: wartość

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

Rodzaj parametru: szablon

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

Argumenty szablonu mogą być:

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.

Jawnie podane 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:

Kolejność argumentów

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

Domyślne argumenty szablonu

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

Domyślne callable

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

Podsumowanie

Quiz