cpp

Wprowadzenie

Możemy specjalizować szablon funkcji czy klasy. Szablon, który specjalizujemy nazywamy szablonem podstawowy (ang. primary template), żeby odróżnić go od specjalizacji, która też jest szablonem. Specjalizacja nadpisuje definicję szablonu podstawowego. Nie można dalej specjalizować specjalizacji.

Szablon podstawowy deklaruje albo definiuje funkcję albo klasę oraz parametry szablonu (liczbę i rodzaj parametrów). Specjalizacja musi mieć tą samą nazwę (funkcji albo klasy) i dostarczyć argumenty dla specjalizowanego szablonu podstawowego.

Specjalizacja szablonu też jest szablonem, ale już dla częściowo albo całkowicie określonych argumentów. Specjalizację możemy poznać po nieco innej składni szablonu, ale ciągle występuje słowo kluczowe template.

Specjalizacja może być częściowa (ang. partial specialization) albo całkowita (ang. complete specialization). Specjalizacja szablonu funkcji nie może być częściowa, może być tylko całkowita. Specjalizacja szablonu klasy może być częściowa albo całkowita.

Specjalizacja szablonu funkcji

Szablon funkcji może być specjalizowany tylko całkowicie, czyli dla ustalonych wszystkich argumentów szablonu podstawowego: szablon specjalizacji nie ma już parametrów, więc jego lista parametrów jest pusta. Tak więc deklaracje i definicje specjalizacji szablonów funkcji rozpoczynają się słowem kluczowym template i pustą listą parametrów:

template <>

Potem następuje definicja szablonu funkcji, która wygląda jak definicja zwykłej funkcji, bo nie używamy w niej (czyli w liście parametrów funkcji i ciele funkcji) nazw parametrów szablonu podstawowego, a jedynie ich ustalonych wartości (np. int, 1 czy std::list). Ale jest pewna różnica.

Różnica dotyczy nazwy funkcji. W specjalizacji podajemy listę argumentów szablonu podstawowego po nazwie funkcji, czego w definicji zwykłej funkcji nie robimy.

Oto przykład:

#include <iostream>

struct A
{
};

// The definition of the primary function template.
template <typename T>
void foo(const T &t)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << t << std::endl;
}

// A complete specialization of a function template.
template <>
void foo<A>(const A &)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << "A" << std::endl;
}

int
main()
{
  foo(1);
  foo(.2);
  foo("Hello!");
  foo(A());
}

Szablon podstawowy i specjalizację możemy także deklarować. Jeżeli zadeklarujemy szablon podstawowy, ale go nie zdefiniujemy, to nie będzie podstawowej implementacji tego szablonu funkcji. Będziemy mogli specjalizować szablon i używać go wyłącznie dla tych specjalizacji. Pokazuje to przykład niżej.

Listę argumentów szablonu podstawowego możemy pominąć, jeżeli kompilator jest w stanie wywnioskować te argumenty na podstawie listy parametrów funkcji. W przykładzie niżej pominęliśmy listę argumentów (<A>) szablonu podstawowego po nazwie funkcji foo w deklaracji i definicji specjalizacji.

#include <iostream>

struct A
{
};

// A declaration of the primary function template.
template <typename T>
void foo(const T &t);

// A declaration of a complete specialization of a function template.
// The compiler deduces the arguments of the primary template.
template <>
void foo(const A &);

// A definition of a complete specialization of a function template.
// The compiler deduces the arguments of the primary template.
template <>
void foo(const A &)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << "A" << std::endl;
}

int
main()
{
  // foo(1);
  foo(A());
}

Nie możemy częściowo specjalizować szablonów funkcji. Specjalizacja częściowa polegałaby na wprowadzeniu parametru dla specjalizacji, ale nie jest to dozwolone, jak pokazuje przykład niżej.

#include <iostream>

template <typename T>
struct C
{
};

// A declaration of the primary function template.
template <typename T>
void foo(const T &t);

// Error: a partial specialization of a function template is not
// allowed.

// template <typename T>
// void foo<C<T>>(const C<T> &)
// {
//   std::cout << __PRETTY_FUNCTION__ << ": ";
//   std::cout << "C<T>" << std::endl;
// }

// This is another primary template.  It's not a specialization.
template <typename T>
void foo(const C<T> &)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << "C<T>" << std::endl;
}

int
main()
{
  // foo(1);
  foo(C<int>());
}

Przykład niżej ilustruje rekurencyjny szablon funkcji, gdzie rekurencja jest przerwana przez specjalizację szablonu. W specjalizacji szablonu musimy podać argument 0 parametru N, bo kompilator nie jest w stanie go wywnioskować. Argument int parametru T może być wywnioskowany, więc nie jest podany.

#include <iostream>

template <typename T>
void print(const T &t)
{
  std::cout << t << '\n';
}

template <unsigned N, typename T>
void print(const T &t)
{
  print(t);
  print<N - 1>(t);
}

// Complete specialization of a function template.
template <>
void print<0>(const int &)
{
}

int
main()
{
  print("Hello!");
  print<10>(1);
}

Przeciążanie funkcji a szablony

Czy możemy się obyć bez szablonów? W przykładzie niżej funkcja foo jest przeciążona, żeby w zależności od argumentu wywołania funkcji dostarczyć inną implementację.

Problem w tym, że nie mamy szablonu, który mógłby być zastosowany dla dowolnego typu i dlatego dla argumentu .2 typu double jest wywołane przeciążenia dla typu int.

#include <iostream>

struct A
{
};

void foo(const int &i)
{
  std::cout << "Function foo: ";
  std::cout << i << std::endl;
}

void foo(const A &)
{
  std::cout << "Function foo: ";
  std::cout << "A" << std::endl;
}

int
main()
{
  foo(1);
  foo('1');
  foo(A());
}

Możemy dołożyć szablon podstawowy do przykładu, jak pokazano niżej. Mamy szablon dla dowolnego typu i przeciążenie dla typu A. Czy dla wywołania funkcji foo z argumentem A() będzie użyty szablon czy przeciążenie? A dokładnie mówiąc przeciążenie funkcji szablonowej (czyli funkcji, która otrzymaliśmy po konkretyzacji podstawowego szablonu funkcji dla T = A) czy przeciążenie zwykłej funkcji? Przeciążenie zwykłej funkcji zawsze ma pierwszeństwo.

#include <iostream>

struct A
{
};

// The definition of the primary function template.
template <typename T>
void foo(const T &t)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << t << std::endl;
}

// A function overload.
void foo(const A &)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << "A" << std::endl;
}

int
main()
{
  foo(1);
  foo(.2);
  foo(A());
}

Możemy dodać także specjalizację dla T = A, ale i tak zostanie wybrane przeciążenie zwykłej funkcji. Podczas wyboru przeciążenia, kompilator nie rozważa specjalizacji, a jedynie przeciążenia zwykłych funkcji i przeciążenia funkcji szablonowych. Tak więc dodanie specjalizacji i tak nie namówi kompilator na jej użycie.

#include <iostream>

struct A
{
};

// The definition of the primary function template.
template <typename T>
void foo(const T &t)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << t << std::endl;
}

// A definition of a complete specialization of a function template.
template <>
void foo(const A &)
{
  std::cout << __PRETTY_FUNCTION__ << ": ";
  std::cout << "A" << std::endl;
}

// A function overload.
// void foo(const A &)
// {
//   std::cout << __PRETTY_FUNCTION__ << ": ";
//   std::cout << "A" << std::endl;
// }

int
main()
{
  foo(1);
  foo(.2);
  foo(A());
}

Kiedy potrzebujemy specjalizacji

Wydaje się, że specjalizacja szablonu jest zbędna, bo tą samą funkcjonalność uzyskaliśmy przeciążając zwykłą funkcję. Jest jednak funkcjonalność specjalizacji, której nie osiągniemy przez przeciążenia.

Specjalizacja szablonów pozwala na zdefiniowanie przez użytkownika funkcji dla kodu, który został już dołączony w pliku nagłówkowym, np. biblioteki szablonowej. Biblioteka deklaruje szablon funkcji, którą potrzebuje, a definicję specjalizacji czy nawet szablonu podstawowego można pozostawić użytkownikowi. Tak może wyglądać plik nagłówkowy library.hpp:

// Declaration of a primary function template.
template <typename T>
void foo(const T &t);

// This function template uses the above declaration.  Without the
// declaration, this template wouldn't compile.
template <typename T>
void goo(const T &t)
{
  foo(t);
}

Tak może wyglądać użycie biblioteki:

#include "library.hpp"

#include <iostream>

// A regular function overload won't cut it.
void
foo(const int &)
{
  std::cout << "overload\n";
}

// We need a specialization of the primary template.
template <>
void
foo(const int &)
{
  std::cout << "specialization\n";
}

int
main()
{
  goo(1);
}

Jeżeli przeciążenie funkcji zadeklarujemy po dołączeniu biblioteki, to funkcja goo nie będzie go znała i nie użyje go. Funkcja wie natomiast, że może użyć szablonu funkcji foo, bo jej szablon podstawowy został zadeklarowany.

Możemy też przenieść definicję przeciążenia funkcji foo przed dyrektywę #include, żeby funkcja goo mogła skorzystać z przeciążenia, ale lepiej nie wprowadzać takiego nieporządku.

Specjalizacja szablonów typów użytkownika

Możemy zadeklarować lub zdefiniować szablon typu użytkownika, czyli struktury, klasy i unii. Ten szablon podstawowy możemy specjalizować całkowicie lub częściowo. Szablon podstawowy i jej specjalizacje mają jedynie wspólną nazwę typu, a ich interfejsy (składowe dostępne użytkownikowi), implementacje i wielkości w pamięci mogą się całkowicie różnić.

Przykładem specjalizacji typu w bibliotece standardowej jest std::vector<bool>, czyli kontenera std::vector dla typu bool. Ta specjalizacja ma podobny interfejs jak szablon podstawowy std::vector, ale zupełnie inną implementację.

Przykład całkowitej specjalizacji

Niżej definiujemy szablon podstawowy typu A z jedną funkcją składową foo. Całkowita specjalizacja dla argumentu double nie ma funkcji foo, a ma funkcję goo i dziedziczy po std::pair. Całkowita specjalizacja typu ma identyczną składnię, jak całkowita specjalizacja funkcji.

#include <iostream>

// Definition of a primary template.
template <typename T>
struct A
{
  void
  foo()
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
};

// A complete specialization.
template<>
struct A<double>: std::pair<double, double>
{
  void
  goo()
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }  
};

int
main()
{
  A<int> i;
  i.foo();
  A<float>().foo();

  // Error: "foo" is in the primary template, which we've overwritten.
  // A<double>().foo();

  // But we can call "goo" alright.
  A<double>().goo();

  // We compare with the < operator inherited from std::pair.
  std::cout << (A<double>() < A<double>()) << std::endl;
}

Częściowa specjalizacja i przykład

W częściowej specjalizacji szablonu typu wprowadzamy parametr, który używamy w definicji argumentu szablonu podstawowego. Lista parametrów specjalizacji nie jest już pusta, jak w przypadku całkowitej specjalizacji.

W przykładzie niżej deklarujemy szablon podstawowy typu A z typowym parametrem T, a następnie definiujemy dwie specjalizacje, obie z parametrem T. Parametry T trzech szablonów nie mają ze sobą nic wspólnego, ponieważ mają lokalny zakres.

Pierwsza specjalizacja definiuje implementację typu A dla przypadków, kiedy argumentem szablonu podstawowego jest std::vector. Pozwalamy na dowolny typ elementów wektora poprzez użycie parametru T specjalizacji.

Druga specjalizacja definiuje implementację typu A dla przypadków, kiedy argumentem szablonu podstawowego jest typ szablonowy, który może być skonkretyzowany z jednym argumentem int.

W funkcji main typ A został użyty z różnymi specjalizacjami. Najciekawszy jest ostatni przypadek, który jest zakomentowany, bo nie może się kompilować: kompilator nie jest w stanie zdecydować, której specjalizacji użyć.

#include <iostream>
#include <list>
#include <vector>
#include <tuple>

// Definition of a primary template.
template <typename T>
struct A;

// A partial specialization for a vector of elements of any type.
template <typename T>
struct A<std::vector<T>>
{
  void
  foo()
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }  
};

// A partial specialization for any template type of the integer
// argument.
template <template<typename...> typename T>
struct A<T<int>>
{
  void
  goo()
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }  
};

int
main()
{
  A<std::vector<bool>>().foo();
  A<std::vector<double>>().foo();
  A<std::list<int>>().goo();
  A<std::tuple<int>>().goo();

  // Ambiguous instantiation: the first or the second specialization?
  // A<std::vector<int>>() a;
}

Najbardziej wyspecjalizowany szablon podstawowy

Specjalizacja (a może lepiej “wyspecjalizowanie”) ma dodatkowe znaczenie w języku C++, który pozwala przeciążać szablony podstawowe funkcji o tej samej nazwie. Jeżeli będzie można użyć więcej niż jednego szablonu, to kompilator wybiera najbardziej wyspecjalizowany szablon. Nie musimy specjalizować szablonu, żeby mówić o najbardziej wyspecjalizowanym szablonie podstawowym. W przykładzie niżej definiujemy dwa szablony podstawowe funkcji foo, a następnie wywołujemy funkcję foo.

#include <iostream>

using namespace std;

// Overload A
template <typename A1>
void foo(A1 a1)
{
  cout << __PRETTY_FUNCTION__ << endl;
}

// Overload B
template <typename B1>
void foo(B1 *b1)
{
  cout << __PRETTY_FUNCTION__ << endl;
}

int main()
{
  int x = 1;
  // Call #1: only overload A is a candidate.  The template function
  // called: void foo(int).
  foo(x);

  // Call #2: overloads A and B are the candidates.  The template
  // function called: void foo(int *).
  foo(&x);
}

Jak kompilator wybrał właściwy szablon dla dwóch wywołań funkcji w przykładzie wyżej? Pierwsze wywołanie przekazuje argument typu całkowitego, więc wybór jest tylko jeden: szablon A. Szablon B nie może być użyty, bo kompilator nie jest w stanie wywnioskować argumentu B1 szablonu tak, żeby można byłoby zainicjalizować parametr b1 funkcji.

Drugie wywołanie jest ciekawsze. Kompilator może użyć zarówno szablonu A, jak i B. W tej sytuacji jest użyty bardziej wyspecjalizowany szablon. W tym przykładzie szablon B jest bardziej wyspecjalizowany.

Zanim przejdziemy dalej (do omówienia idei bardziej wyspecjalizowanego szablonu), to podsumujmy przykład i zauważmy ważny fakt. Podczas opracowania drugiego wywołania, kompilator może skonkretyzować oba szablony używając wywnioskowanych argumentów:

Obie konkretyzacje tworzą funkcję szablonową void foo(int *), którą można już użyć w drugim wywołaniu. Teraz problemem pozostaje, który szablon wybrać do wygenerowania ciała tej funkcji szablonowej.

Podkreślmy, że kompilator w dwóch osobnych krokach:

  1. tworzy zbiór kandydatów szablonów,

  2. wybiera najlepszy (najbardziej wyspecjalizowany) szablon ze zbioru kandydatów.

W pierwszym kroku, spośród dostępnych szablonów, kompilator wybiera te, dla których można wywnioskować argumenty (pozostałe szablony są ignorowane zgodnie z zasadą SFINAE). Zwróćmy uwagę na ważny fakt: w drugim kroku, wyrażenie wywołania nie jest już brane pod uwagę. Wyrażenie wywołania jest brane pod uwagę tylko w pierwszym kroku, żeby wybrać kandydatów.

Wybór najlepszego kandydata

Ze zbioru kandydatów wybieramy najlepszy szablon, czyli najbardziej wyspecjalizowany. Wyboru dokonujemy przez porównywanie szablonów parami, czyli z użyciem binarnej relacji, którą oznaczymy jako <. Porównując parę szablonów I i J chcemy określić, który szablon jest bardziej wyspecjalizowany. Zapis I < J czytamy: I jest bardziej wyspecjalizowany niż J.

Jednak relacja < nie musi zachodzić między każdą parą szablonów i dlatego nazywana jest częściową. Ponieważ relacja jest częściowa, to może okazać się, że kompilator nie jest w stanie wybrać najbardziej wyspecjalizowanego szablonu i wted zgłasza błąd niejednoznaczności.

Relacja < jest silnym porządkiem częściowym, ponieważ jest:

Dla przykładu wyżej możemy powiedzieć, że dla drugiego wywołania szablon B jest bardziej wyspecjalizowany niż A (relacja zachodzi: B < A). Ponieważ zbiór kandydatów ma tylko dwa szablony, więc szablon B uznajemy za najbardziej wyspecializowany.

Relacja “bardziej wyspecjalizowany”

Relacja I < J porównuje argumenty wywołania funkcji, które mogą być użyte z szablonami I i J. Szablon bardziej wyspecjalizowany to ten, którego dopuszczalne argumenty funkcji są podzbiorem właściwym dopuszczalnych argumentów funkcji drugiego szablonu. Parafrazując: I < J oznacza, że każdy argument funkcji dopuszczalny dla I jest też dopuszczalny dla J, ale nie na odwrót (czyli nie każdy argument funkcji dopuszczalny dla J jest dopuszczalny dla I). Ale co dokładnie oznacza “dopuszczany argument funkcji”?

Dopuszczalny argument funkcji to ten, który może być użyty do wywołania funkcji, czyli (w przypadku użycia szablonu funkcji) ten, na podstawie którego można wywnioskować argumenty szablonu. Nie chodzi tylko o typ argumentu, ale i jego kategorię (co jest ważne w przypadku inicjalizacji referencyjnego parametru funkcji). W drugim kroku wiemy, że argument funkcji opracowywanego wyrażenia jest dopuszczalny dla każdego szablonu ze zbioru kandydatów: ten argument nie pozwoli nam wybrać bardziej wyspecjalizowanego szablonu i dlatego nie jest dalej brany pod uwagę (w pierwszym kroku był, ale w drugim już nie). Potrzebujemy innego sposobu porównania.

W przykładzie wyżej, funkcja szablonu A przyjmuje argumenty (funkcji) dowolnych typów, także wskaźnikowych. Funkcja szablonu B przyjmuje argumenty tylko typów wskaźnikowych. Szablon B jest bardziej wyspecjalizowany niż szablon A, ponieważ zbiór dopuszczalnych argumentów funkcji szablonu B jest podzbiorem właściwym zbioru dopuszczalnych argumentów funkcji szablonu A. Tak to wynika z wnioskowania:

Podsumowanie

Quiz