Szablony

Szablony implementują koncepcję programowania generycznego (generic programming) - programowania używającego typów danych jako parametrów.

Szablony zapewniają mechanizm, za pomocą którego funkcje lub klasy mogą być implementowane dla ogólnych typów danych (nawet takich, które jeszcze nie istnieją).

  • zastosowanie szablonów klas umożliwia parametryzację kontenerów ze względu na typ elementów, które są w nich przechowywane
  • szablony funkcji umożliwiają implementację algorytmów działających w ogólny sposób dla różnych struktur danych

W szczególnych przypadkach implementacja szablonów klas lub funkcji może być wyspecjalizowana dla pewnych typów.

Parametry szablonów mogą być:

  • typami
  • stałymi typów całkowitych lub wyliczeniowych znanymi w chwili kompilacji
  • szablonami

Szablony funkcji

Szablon funkcji - funkcja, której typy argumentów lub typ zwracanej wartości zostały sparametryzowane.

Składnia definicji szablonu funkcji:

template < comma_separated_list_of_parameters >
return_type function_name(list_of_args)
{
    //...
}

Przykład definicji szablonu funkcji max():

template<typename T>
T max(T a, T b)
{
    return b < a ? a : b;
}

Użycie szablonu funkcji max():

int val1 = ::max<int>(4, 9);

double x = 4.50;
double y = 3.14;
double val2 = ::max(x, y);

std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "max(s1, s2) = " << ::max(s1, s2) << '\n';

Dedukcja typu argumentów szablonu

W momencie wywołania funkcji szablonowej max() następuje proces dedukcji tych typów argumentów szablonu, które nie zostały podane w ostrych nawiasach <>. Typy argumentów szablonu dedukowane są na podstawie typu argumentów funkcji.

Reguły dedukcji typów dla szablonów funkcji:

  • Każdy parametr funkcji może brać udział (lub nie) w procesie dedukcji parametru szablonu
  • Wszystkie dedukcje są przeprowadzane niezależnie od siebie
  • Na zakończenie procesu dedukcji kompilator sprawdza, czy każdy parametr szablonu został wydedukowany i czy nie ma konfliktów między wydedukowanymi parametrami
  • Każdy parametr funkcji, który bierze udział w procesie dedukcji musi ściśle pasować do typu argumentu funkcji - niejawne konwersje są zabronione
template<typename T, typename U>
void f(T x, U y);

template<typename T>
void g(T x, T y);

int main()
{
    f(1, 2); // void f(T, U) [T = int, U = int]
    g(1, 2); // void g(T, T) [T = int]
    g(1, 2u); // error: no matching function for call to g(int, unsigned int)
}

W przypadku, gdy w procesie dedukcji wykryte zostaną konflikty:

short f();

auto val = ::max(f(), 42); // ERROR - no matching function

istnieją dwa rozwiązania:

// #1
auto val1 = ::max(static_cast<int>(f()), 42); // OK

// #2
auto val2 = ::max<int>(f(), 42); // OK

Tworzenie instancji szablonu

Koncepcja szablonów wykracza poza zwykły model kompilacji (konsolidacji). Cały kod szablonu powinien być umieszczony w jednym pliku nagłówkowym. Dołączając następnie zawartość pliku nagłówkowego do kodu aplikacji umożliwiamy generację i kompilację kodu dla konkretnych typów.

Tworzenie instancji szablonu - proces, w którym na podstawie szablonu generowany jest kod, który zostanie skompilowany.

Utworzenie instancji szablonu jest możliwe tylko wtedy, gdy dla typu podanego jako argument szablonu zdefiniowane są wszystkie operacje używane przez szablon, np. operatory <, ==, !=, wywołania konkretnych metod, itp.

Fazy kompilacji szablonu

Proces kompilacji szablonu przebiega w dwóch fazach:

  1. Na etapie definicji szablonu, ale bez tworzenia jego instancji kod jest sprawdzany pod względem poprawności bez uwzględniania parametrów szablonu:
    • wykrywane są błędy składniowe
    • wykrywane jest użycie nieznanych nazw (typów, funkcji, itp. ), które nie zależą od parametru szablonu
    • sprawdzane są statyczne asercje, które nie zależą od parametru szablonu
  2. Podczas tworzenia instancji szablonu, kod szablonu jest jeszcze raz sprawdzany. W szczególności sprawdzane są części, które zależą od parametrów szablonu.
template<typename T>
void foo(T t)
{
    undeclared();   // first-phase compile-time error if undeclared() unknown
    undeclared(t);  // second-phase compile-time error if undeclared(T) unknown
    static_assert(sizeof(int) > 10, "int too small");  // always fails if sizeof(int)<=10
    static_assert(sizeof(T) > 10, "T too small");  // fails if instantiated for T with size <=10
}

Specjalizacja funkcji szablonowych

W specjalnych przypadkach istnieje możliwość zdefiniowania funkcji specjalizowanej.

Zdefiniujmy najpierw pierwszy szablon funkcji:

template <typename T>
bool is_greater(T a, T b)
{
    return a > b;
}

Jeśli wywołamy ten szablon dla literałów znakowych „abc” i „def” utworzona zostanie instancja szablonu, która porówna adresy przechowywane we wskaźnikach a nie tekst. Aby zapewnić prawidłowe porównanie c-łańcuchów musimy dostarczyć specjalizowaną wersję szablonu dla parametru const char*:

template <>
bool is_greater<const char*>(const char* a, const char* b)
{
    return strcmp(a, b) > 0;
}

is_grater(4, 5); // wywołanie funkcji szablonowej
//...
is_greater("a", "g"); // wywołanie funkcji specjalizowanej dla const char*

Ponieważ podawanie typu specjalizowanego w ostrych nawiasach jest redundantne można specjalizację szablonu funkcji zapisać w sposób następujący:

template <>
bool is_greater(const char* a, const char* b)
{
    return strcmp(a, b) > 0;
}

W praktyce zamiast stosować jawną specjalizację szablonu funkcji można wykorzystać przeciążenie funkcji is_greater():

bool is_greater(const char* a, const char* b)
{
    return strcmp(a, b) > 0;
}

Połączenie przeciążania szablonów, specjalizacji i przeciążania funkcji

Przykład wykorzystania specjalizacji lub przeciążenia szablonu funkcji:

template <typename T> T sqrt(T); // basic template sqrt<T>
template <> float sqrt(float);   // specialization of template sqrt<float>
template <typename T> complex<T> sqrt(complex<T>);  // overloaded template sqrt for complex<T> arguments
double sqrt(double); // overloaded function sqrt(double)

//...

void f(complex<double> z)
{
   sqrt(2);   // sqrt<int>(int)
   sqrt(2.0); // sqrt(double)
   sqrt(z);   // sqrt<double>(complex<double>)
   sqrt(3.14f); // sqrt<float>(float)
}

Przeciążanie szablonów

W programie może obok siebie istnieć mając tę samą nazwę:

  • kilka szablonów funkcji – byle produkowały funkcje o odmiennych argumentach,
  • funkcje, o argumentach takich, że mogłyby zostać wyprodukowane przez któryś z szablonów (funkcje specjalizowane),
  • funkcje o argumentach takich, że nie mógłby ich wyprodukować żaden z istniejących szablonów (zwykłe przeładowanie).

Adres wygenerowanej funkcji

Możliwe jest pobranie adresu funkcji wygenerowanej na podstawie szablonu.

template <typename T> void f(T* ptr)
{
   cout << "funkcja szablonowa f(T*)" << endl;
}

void h(void (*pf)(int*))
{
   cout << "h( void (*pf)(int*)" << endl;
}

int main()
{
   h(&f<int>);    // przekazanie adresu funkcji wygenerowanej
                  // na podstawie szablonu
}

Parametry szablonu dla wartości zwracanych przez funkcję

W przypadku, gdy funkcja szablonowa ma zwrócić typ inny niż typy argumentów (lub przynajmniej rozważana jest taka możliwość) możemy zastosować następujące rozwiązania:

Parametr szablonu określający zwracany typ

template<typename TReturn, typename T1, typename T2>
TReturn max (T1 a, T2 b);

Ponieważ nie ma związku pomiędzy typami argumentów a typem zwracanym, wywołując szablon należy określić jawnie typ zwracany.

::max<double>(4, 7.2);

Dedukcja typu zwracanego z funkcji (C++14)

template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
    return b < a ? a : b;
}

Użycie klasy cech (type traits)

#include <type_traits>
template<typename T1, typename T2>
std::common_type_t<T1, T2> max (T1 a, T2 b)
{
    return b < a ? a : b;
}

Domyślne parametry szablonu

Definiują parametry szablonu, możemy określić dla nich wartości domyślne. Mogą one odwoływać się do wcześniej zdefiniowanych parametrów szablonu.

#include <type_traits>

template<typename T1, typename T2, typename RT = std::common_type_t<T1,T2>>
RT max (T1 a, T2 b)
{
    return b < a ? a : b;
}

Wywołując szablon funkcji możemy pominąć argumenty z domyślnymi wartościami:

auto val_1 = ::max(1, 3.14); // ::max<int, double, double>

lub jawnie podać odpowiedni argument:

auto val_2 = ::max<int, short, double>(1, 4);

Szablony klas

Podobnie do funkcji, klasy oraz struktury też mogą być być szablonami sparametryzowanymi typami.

Szablony klas mogą być wykorzystane do implementacji kontenerów, które mogą przechowywać dane typów, które będą definiowane później.

W terminologii obiektowej szablony klas nazywane są klasami parametryzowanymi.

W przypadku użycia szablonów klas generowany jest kod tylko dla tych funkcji składowych, które rzeczywiście są wywoływane.

template <typename T>
class Vector
{
    size_t size_;
    T* items_;

public:
    explicit Vector(size_t size);
    ~Vector() { delete [] items_; }

    //...

    const T& operator[](size_t index) const
    {
         return items_[index];
    }

    T& operator[](size_t index)
    {
         return items_[index];
    }

    const T& at(size_t index) const;
    T& at(size_t index);

    size_t size() const
    {
        return size_;
    }
};

Aby utworzyć zmienne typów szablonowych musimy określić parametry szablonu klasy:

Vector<int> integral_numbers(100);
Vector<double> real_numbers(200);
Vector<std::string> words(665);
Vector<Vector<int>> matrix(10);

Implementacja funkcji składowych

Definiując funkcję składową szablonu klasy należy określić jej przynależność do szablonu.

template <typename T>
Vector<T>::Vector(size_t size)
    : size_{size}, items_{new T[size]}
{
}

template <typename T>
T& Vector<T>::at(size_t index)
{
    if (index >= size_)
        throw std::out_of_range("Vector::operator[]");

    return items_[index];
}

template <typename T>
const T& Vector<T>::at(size_t index) const
{
    if (index >= size_)
        throw std::out_of_range("Vector::operator[]");

    return items_[index];
}

Parametry szablonów klas

Każdy parametr szablonu może być:

  1. Typem (wbudowanym lub zdefiniowanym przez użytkownika).
  2. Stałą znaną w chwili kompilacji (liczby całkowite, wskaźniki i referencje danych statycznych).
  3. Innym szablonem.

Parametry szablonów niebędące typami

Można używać parametrów nie będących typami, o ile są to wartości znane na etapie kompilacji.

template <class T, size_t N>
struct Array
{
    using value_type = T;

    T items_[N];

    constexpr size_t size()
    {
        return N;
    }
};

Array<int, 1024> buffer;

Parametrom szablonu klasy można przypisać argumenty domyślne (od C++11 jest to możliwe również dla szablonów funkcji).

template <class T, size_t N = 1024>
struct Array
{
public:
    using value_type = T;

    constexpr size_t size()
    {
        return N;
    }

    T items_[N];
};

template <typename T, typename Container = std::vector<T>>
class Stack
{
public:
    Stack();
    void push(const T& elem);
    void push(T&& elem);
    void pop();
    T& top() const;
private:
    Container items_;
};

// creating objects
Stack<int> stack_one; // Stack<int, vector<int>>
Stack<int, std::list<int>> stack_two;

Szablony jako parametry szablonów

Jeżeli w kodzie jako parametr ma być użyty inny szablon, to kompilator powinien zostać o tym poinformowany.

template <typename T, template<typename, size_t> class Container, size_t N = 1024>
class Stack
{
private:
    Container<T, N> elems_;
public:
    Stack();
    void push(const T& elem);
    void push(T&& elem);
    void pop();
    T& top() const;
};

template <typename T, template<typename, size_t> class Container, size_t N>
Stack<T, Container, N>::Stack()
{
}

// creating objects
Stack<int, Array> stack_three;
Stack<int, Array, 512> stack_four;

Specjalizacja szablonów klas

Specjalizacja szablonów klas:

  • polega na osobnej implementacji szablonów dla wybranych typów argumentów.
  • umożliwia optymalizację implementacji dla wybranych typów lub uniknięcie niepożądanego zachowania na skutek utworzenia instancji szablonu dla określonego typu.
template <typename T> class Pair { /* ... */ };     // szablon ogólny
template<typename T> class Pair<T*> { /* ... */ };  // częściowa specjalizacja
template<> class Pair<const char*>{ /* ... */ };    // pełna specjalizacja

Pełna specjalizacja szablonu klasy

Deklaracja pełnej specjalizacji wymaga podania:

template <>
class Vector<bool>
{
    //...
};

Jeśli specjalizujemy szablon klasy, to musimy zapewnić wyspecjalizowaną implementację dla wszystkich funkcji składowych.

Możliwe jest natomiast rozszerzenie interfejsu klasy o dodatkowe składowe (np. std::vector<bool> definiuje dodatkowe metody flip())

Specjalizacja częściowa szablonu klasy

Dla szablonów klas (w odróżnieniu od szablonów funkcji) możliwe jest tworzenie częściowych specjalizacji szablonów.

Dla szablonu klasy:

template <class T1, class T2>
class MyClass
{
  //...
};

możemy utworzyć następujące specjalizacje częściowe:

template <typename T>
class MyClass<T, T>
{
  // specjalizacja częściowa: drugim typem jest T
};
template <typename T>
class MyClass<T, int>
{
    // specjalizacja częściowa: drugim typem jest int
};
template <typename T1, typename T2>
class MyClass<T1*, T2*>
{
    // oba parametry są wskaźnikami
};

Poniższe przykłady pokazują, które wersje szablonu klasy zostaną utworzone:

MyClass<int, float> mif;      // uses MyClass<T1,T2>
MyClass<float, float> mff;    // uses MyClass<T,T>
MyClass<float, int> mfi;      // uses MyClass<T,int>
MyClass<int*, float*> mp;     // uses MyClass<T1*,T2*>

W przypadku, gdy więcej niż jedna specjalizacja pasuje wystarczająco dobrze zgłaszany jest błąd dwuznaczności:

MyClass<int, int> me1; // ERROR: matches MyClass<T, T> & MyClass<T, int>

MyClass<int*, int*> me2; // ERROR: matches MyClass<T, T> & MyClass<T1*, T1*>

Składowe jako szablony

Składowe klas mogą być szablonami. Dotyczy to:

  • wewnętrznych klas pomocniczych,
  • funkcji składowych.
template <typename T>
class Stack
{
    std::deque<T> items_;
public:
    void push(const T&);
    void pop();
    //...
    // przypisanie stosu o elementach typu T2
    template <typename T2>
    Stack<T>& operator=(const Stack<T2>&);
};

template <typename T>
    template <typename T2>
Stack<T>& Stack<T>::operator=(const Stack<T2>& source)
{
    //...
}

Nazwy zależne od typów

Gramatyka języka C++ nie jest niezależna od kontekstu. Aby sparsować np. definicję funkcji, potrzebna jest znajomość kontekstu, w którym funkcja jest definiowana.

Przykład problemu:

template <typename T>
auto dependent_name_context1(int x)
{
    auto value = T::A(x);

    return value;
}

Standard C++ rozwiązuje problem przyjmując założenie, że dowolna nazwa, która jest zależna od parametru szablonu odnosi się do zmiennej, funkcji lub obiektu.

struct S1
{
    static int A(int v) { return v * v; }
};

auto value2 = dependent_name_context1<S1>(10); // OK - T::A(x) was parsed as a function call

Słowo kluczowe typename umożliwia określenie, że dany symbol (identyfikator) występujący w kodzie szablonu i zależny od parametru szablonu jest typem, np. typem zagnieżdżonym – zdefiniowanym wewnątrz klasy.

struct S2
{
    struct A
    {
        int x;

        A(int x) : x{x}
        {}
    };
};

template <typename T>
auto dependent_name_context2(int x)
{
    auto value = typename T::A(x); // hint for a compiler that A is a type

    return value;
}

auto value = dependent_name_context2<S2>(10); // OK - T::A was parsed as a nested type

Przykład użycia słowa kluczowego typename dla zagnieżdżonych typów definiowanych w kontenerach:

template <class T>
class Vector
{
public:
    using value_type = T;
    using iterator = T*;
    using const_iterator = const T*;

    // ...
    // rest of implementation
};

template <typename Container>
typename Container::value_type sum(const Container& container)
{
    using result_type = typename Container::value_type;

    result_type result{};

    for(typename Container::const_iterator it = container.begin(); it != container.end(); ++it)
        result += *it;

    return result;
}

Podobne problemy dotyczą również nazw zależnych od parametrów szablonu i odwołujących się do zagnieżdżonych definicji innych szablonów:

struct S1 { static constexpr int A = 0; }; // S1::A is an object
struct S2 { template<int N> static void A(int) {} }; // S2::A is a function template
struct S3 { template<int N> struct A {}; }; // S3::A is a class template
int x;

template<class T>
void foo()
{
    T::A < 0 > (x); // if T::A is an object, this is a pair of comparisons;
                    // if T::A is a typename, this is a syntax error;
                    // if T::A is a function template, this is a function call;
                    // if T::A is a class or alias template, this is a declaration.
}

foo<S1>(); // OK

Aby określić, że dany symbol zależny od parametru szablonu to szablon funkcji piszemy:

template <typename T>
voi foo()
{
    T::template A<0>();
}

Aby określić, że dany symbol zależny od parametru szablonu to szablon klasy piszemy:

template <typename T>
voi foo()
{
    typename T::template A<0>();
}

Dedukcja argumentów szablonu dla klas

C++17 wprowadza mechanizm dedukcji argumentów szablonu klasy (Class Template Argument Deduction). Typy parametrów szablonu klasy mogą być dedukowane na podstawie argumentów przekazanych do konstruktora tworzonego obiektu.

template <typename T>
class complex
{
    T re_, img_;
public:
    complex(T re, T img) : re_{re}, img_{img}
    {}
};

auto c1 = complex<int>{5, 2}; // OK - all versions of C++ standard

auto c2 = complex{5, 3}; // OK since C++17 - compiler deduces complex<int>

auto c3 = complex(5.1, 6.5); // OK in C++17 - compiler deduces complex<double>

auto c4 = complex{5, 4.1}; // ERROR - args don't have the same type
auto c5 = complex(5, 4.1); // ERROR - args don't have the same type

Ostrzeżenie

Nie można częściowo dedukować argumentów szablonu klasy. Należy wyspecyfikować lub wydedukować wszystkie parametry z wyjątkiem parametrów domyślnych.

Praktyczny przykład dedukcji argumentów szablonu klasy:

std::mutex mtx;

std::lock_guard lk{mtx}; // deduces std::lock_guard<std::mutex>

Podpowiedzi dedukcyjne (deduction guides)

C++17 umożliwia tworzenie podpowiedzi dla kompilatora, jak powinny być dedukowane typy parametrów szablonu klasy na podstawie wywołania odpowiedniego konstruktora.

Daje to możliwość poprawy/modyfikacji domyślnego procesu dedukcji.

Dla szablonu:

template <typename T>
class S
{
private:
    T value;
public:
    S(T v) : value(v)
    {}
};

Podpowiedź dedukcyjna musi zostać umieszczona w tym samym zakresie (przestrzeni nazw) i może mieć postać:

template <typename T> S(T) -> S<T>; // deduction guide

gdzie:

  • S<T> to tzw. typ zalecany (guided type)
  • nazwa podpowiedzi dedukcyjnej musi być niekwalifikowaną nazwą klasy szablonowej zadeklarowanej wcześniej w tym samym zakresie
  • typ zalecany podpowiedzi musi odwoływać się do identyfikatora szablonu (template-id), do którego odnosi się podpowiedź

Użycie podpowiedzi:

S x{12}; // OK -> S<int> x{12};
S y(12); // OK -> S<int> y(12);
auto z = S{12}; // OK -> auto z = S<int>{12};
S s1(1), s2{2}; // OK -> S<int> s1(1), s2{2};
S s3(42), s4{3.14}; // ERROR

W deklaracji S x{12}; specyfikator S jest nazywany symbolem zastępczym dla klasy (placeholder class type).

W przypadku użycia symbolu zastępczego dla klasy, nazwa zmiennej musi zostać podana jako następny element składni. W rezultacie poniższa deklaracja jest błędem składniowym:

S* p = &x; // ERROR - syntax not permitted

Dany szablon klasy może mieć wiele konstruktorów oraz wiele podpowiedzi dedukcyjnych:

template <typename T>
struct Data
{
    T value;

    using type1 = T;

    Data(const T& v)
        : value(v)
    {
    }

    template <typename ItemType>
    Data(initializer_list<ItemType> il)
        : value(il)
    {
    }
};

template <typename T>
Data(T)->Data<T>;

template <typename T>
Data(initializer_list<T>)->Data<vector<T>>;

Data(const char*) -> Data<std::string>;

//...

Data d1("hello"); // OK -> Data<string>

const int tab[10] = {1, 2, 3, 4};
Data d2(tab); // OK -> Data<const int*>

Data d3 = 3; // OK -> Data<int>

Data d4{1, 2, 3, 4}; // OK -> Data<vector<int>>

Data d5 = {1, 2, 3, 4}; // OK -> Data<vector<int>>

Data d6 = {1}; // OK -> Data<vector<int>>

Data d7(d6); // OK - copy by default rule -> Data<vector<int>>

Data d8{d6, d7}; // OK -> Data<vector<Data<vector<int>>>>

Podpowiedzi dedukcyjne nie są szablonami funkcji - służą jedynie dedukowaniu argumentów szablonu i nie są wywoływane. W rezultacie nie ma znaczenia czy argumenty w deklaracjach dedukcyjnych są przekazywane przez referencje, czy nie.

template <typename T>
struct X
{
    //...
};

template <typename T>
struct Y
{
    Y(const X<T>&);
    Y(X<T>&&);
};

template <typename T> Y(X<T>) -> Y<T>; // deduction guide without references

W powyższym przykładzie podpowiedź dedukcyjna nie odpowiada dokładnie sygnaturom konstruktorów przeciążonych. Nie ma to znaczenia, ponieważ jedynym celem podpowiedzi jest umożliwienie dedukcji typu, który jest parametrem szablonu. Dopasowanie wywołania przeciążonego konstruktora odbywa się później.

Niejawne podpowiedzi dedukcyjne

Ponieważ często podpowiedź dedukcyjna jest potrzebna dla każdego konstruktora klasy, standard C++17 wprowadza mechanizm niejawnych podpowiedzi dedukcyjnych (implicit deduction guides). Działa on w następujący sposób:

  • Lista parametrów szablonu dla podpowiedzi zawiera listę parametrów z szablonu klasy - w przypadku szablonowego konstruktora klasy kolejnym elementem jest lista parametrów szablonu konstruktora klasy
  • Parametry „funkcyjne” podpowiedzi są kopiowane z konstruktora lub konstruktora szablonowego
  • Zalecany typ w podpowiedzi jest nazwą szablonu z argumentami, które są parametrami szablonu wziętymi z klasy szablonowej

Dla klasy szablonowej rozważanej powyżej:

template <typename T>
class S
{
private:
    T value;
public:
    S(T v) : value(v)
    {}
};

niejawna podpowiedź dedukcyjna będzie wyglądać następująco:

template <typename T> S(T) -> S<T>; // implicit deduction guide

W rezultacie programista nie musi implementować jej jawnie.

Specjalny przypadek dedukcji argumentów klasy szablonowej

Rozważmy następujący przypadek dedukcji:

S x{42}; // x has type S<int>

S y{x};
S z(x);

W obu przypadkach dedukowany typ zmiennych y i z to S<int>.

W tym przypadku mechanizm dedukcji argumentów klasy szablonowej realizuje kopię obiektu.

  • dla deklaracji S<T> x; S{x} dedukuje typ: S<T>{x} zamiast S<S<T>>{x}

W niektórych przypadkach może być to zaskakujące i kontrowersyjne:

std::vector v{1, 2, 3}; // vector<int>
std::vector data1{v, v}; // vector<vector<int>>
std::vector data2{v}; // vector<int>!

W powyższym kodzie dedukcja argumentów szablonu vector zależy od ilości argumentów przekazanych do konstruktora!

Agregaty a dedukcja argumentów

Jeśli szablon klasy jest agregatem, to mechanizm automatycznej dedukcji argumentów szablonu wymaga napisania jawnej podpowiedzi dedukcyjnej.

Bez podpowiedzi dedukcyjnej dedukcja dla agregatów nie działa:

template <typename T>
struct Aggregate1
{
    T value;
};

Aggregate1 agg1{8}; // ERROR
Aggregate1 agg2{"eight"}; // ERROR
Aggregate1 agg3 = 3.14; // ERROR

Gdy napiszemy dla agregatu podpowiedź, to możemy zacząć korzystać z mechanizmu dedukcji:

template <typename T>
struct Aggregate2
{
    T value;
};

template <typename T>
Aggregate2(T) -> Aggregate2<T>;

Aggregate2 agg1{8}; // OK -> Aggregate2<int>
Aggregate2 agg2{"eight"}; // OK -> Aggregate2<const char*>
Aggregate2 agg3 = { 3.14 }; // OK -> Aggregate2<double>

Podpowiedzi dedukcyjne w bibliotece standardowej

Dla wielu klas szablonowych z biblioteki standardowej dodano podpowiedzi dedukcyjne w celu ułatwienia tworzenia instancji tych klas.

std::pair<T>

Dla pary STL dodana w standardzie podpowiedź to:

template<class T1, class T2>
pair(T1, T2) -> pair<T1, T2>;

pair p1(1, 3.14); // -> pair<int, double>

pair p2{3.14f, "text"s}; // -> pair<float, string>

pair p3{3.14f, "text"}; // -> pair<float, const char*>

int tab[3] = { 1, 2, 3 };
pair p4{1, tab}; // -> pair<int, int*>
std::tuple<T…>

Szablon std::tuple jest traktowany podobnie jak std::pair:

template<class... UTypes>
tuple(UTypes...) -> tuple<UTypes...>;

template<class T1, class T2>
tuple(pair<T1, T2>) -> tuple<T1, T2>;

//... other deduction guides working with allocators

int x = 10;
const int& cref_x = x;

tuple t1{x, &x, cref_x, "hello", "world"s}; -> tuple<int, int*, int, const char*, string>
std::optional<T>

Klasa std::optional jest traktowana podobnie do pary i krotki.

template<class T> optional(T) -> optional<T>;

optional o1(3); // -> optional<int>
optional o2 = o1; // -> optional<int>
Inteligentne wskaźniki

Dedukcja dla argumentów konstruktora będących wskaźnikami jest zablokowana:

int* ptr = new int{5};
unique_ptr uptr{ip}; // ERROR - ill-formed (due to array type clash)

Wspierana jest dedukcja przy konwersjach:

  • z weak_ptr/unique_ptr do shared_ptr:

    template <class T> shared_ptr(weak_ptr<T>) ->  shared_ptr<T>;
    template <class T, class D> shared_ptr(unique_ptr<T, D>) ->  shared_ptr<T>;
    
  • z shared_ptr do weak_ptr

    template<class T> weak_ptr(shared_ptr<T>) -> weak_ptr<T>;
    
unique_ptr<int> uptr = make_unique<int>(3);

shared_ptr sptr = move(uptr); -> shared_ptr<int>

weak_ptr wptr = sptr; // -> weak_prt<int>

shared_ptr sptr2{wptr}; // -> shared_ptr<int>
std::function

Dozwolone jest dedukowanie sygnatur funkcji dla std::function:

int add(int x, int y)
{
    return x + y;
}

function f1 = &add;
assert(f1(4, 5) == 9);

function f2 = [](const string& txt) { cout << txt << " from lambda!" << endl; };
f2("Hello");
Kontenery i sekwencje

Dla kontenerów standardowych dozwolona jest dedukcja typu kontenera dla konstruktora akceptującego parę iteratorów:

vector<int> vec{ 1, 2, 3 };
list lst(vec.begin(), vec.end()); // -> list<int>

Dla std::array dozwolona jest dedukcja z sekwencji:

std::array arr1 { 1, 2, 3 }; // -> std::array<int, 3>

Aliasy szablonów

Aliasy typów

W C++11 deklaracja using może zostać użyta do tworzenia bardziej czytelnych aliasów dla typów - zamiennik dla typedef.

using CarID = int;

using Func = int(*)(double, double);

using DictionaryDesc = std::map<std::string, std::string, std::greater<std::string>>;

Aliasy szablonów

Aliasy typów mogą być parametryzowane typem. Można je wykorzystać do tworzenia częściowo związanych typów szablonowych.

template <typename T>
using StrKeyMap = std::map<std::string, T>;

StrKeyMap<int> my_map; // std::map<std::string, int>

Parametrem aliasu typu szablonowego może być także stała znana w czasie kompilacji:

template <std::size_t N>
using StringArray = std::array<std::string, N>;

StringArray<255> arr1;

Aliasy szablonów nie mogą być specjalizowane.

template <typename T>
using MyAllocList = std::list<T, MyAllocator>;

template <typename T>
using MyAllocList = std::list<T*, MyAllocator>; // error

Przykład utworzenia aliasu dla szablonu klasy smart pointer’a:

template <typename Stream>
struct StreamDeleter
{
    void operator()(Stream* os)
    {
        os->close();
        delete os;
    }
}

template <typename Stream>
using StreamPtr = std::unique_ptr<Stream, StreamDeleter<Stream>>;

// ...
{
    StreamPtr<ofstream> p_log(new ofstream("log.log"));
    *p_log << "Log statement";
}

Aliasy i typename

Od C++14 biblioteka standardowa używa aliasów dla wszystkich cech typów, które zwracają typ.

template <typename T>
using is_void_t = typename is_void<T>::type;

W rezultacie kod odwołujący się do cechy:

typename is_void<T>::type

możemy uprościć do:

is_void_t<T>;

Szablony zmiennych

W C++14 zmienne mogą być parametryzowane przy pomocy typu. Takie zmienne nazywamy zmiennymi szablonowymi (variable templates).

template<typename T>
constexpr T pi{3.1415926535897932385};

Aby użyć zmiennej szablonowej, należy podać jej typ:

std::cout << pi<double> << '\n';
std::cout << pi<float> << '\n';

Parametrami zmiennych szablonowych mogą być stałe znane na etapie kompilacji:

template<int N>
std::array<int, N> arr{};

int main()
{
    arr<10>[0] = 42; // sets first element of global arr

    for (std::size_t i = 0; i < arr<10>.size(); ++i)
    {
        // uses values set in arr
        std::cout << arr<10>[i] << '\n';
    }
}

Zmienne szablonowe a jednostki translacji

Deklaracja zmiennych szablonowych może być używana w innych jednostkach translacji.

  • plik - header.hpp

    template<typename T> T val{};     // zero initialized value
    
  • plik - „unit1.cpp”

    #include "header.hpp"
    
    int main()
    {
        val<long> = 42;
        print();
    }
    
  • plik - „unit2.cpp”

    void print()
    {
        std::cout << val<long> << '\n'; // OK: prints 42
    }