--- title: Шаблоны tags: cpp, mipt, 3sem, teaching, templates --- # Пятое занятие #### 2022-10-01 ## Шаблоны ### Идея шаблонов Иногда для требуется написать функцию/класс, который бы работал не только с одним типом данных, а мог бы выполнять те же действия для почти любых типов данных. В этом случае можно воспользоваться обобщенным программированием (как на первом семинаре) или использовать встроенные средства языка, а именно - шаблоны. ### Шаблонные функции Первая же идея использования шаблонов возникает при написании функций. Например, функция максимума из двух операндов может выглядеть следующим образом. ```cpp= int max(int a, int b) { return a > b ? a : b; } double max(double a, double b) { return a > b ? a : b; } ``` Однако, неудобно для каждого нового типа снова писать новую версию функции. В этом случае как раз помогают шаблоны. Синтаксис шаблонов: ```cpp= template<typename T> // keyword for templates T mymax(T a, T b) { return a > b ? a : b; } int main() { std::cout << mymax<int>(1, 2) << '\n'; // class MyClass must have operator '<' std::cout << mymax<MyClass>(MyClass(5), MyClass(10)) << '\n'; } ``` **Замечание 1:** Ключевое слово ```template``` действует только на ближайший блок кода (функцию или класс) **Замечание 2:** Данные функции будут созданы только при их вызове. **Замечание 3:** После объявления шаблона может идти только объявление. **Замечание 4:** Объявление шаблонов возможно только в глобальной видимости, внутри класса или ```namespace```. Внутри функции нельзя объявлять новые шаблоны. **Примечание 1:** Вместо ```typename``` можно использовать ```class``` по историческим причинам ### Перегрузка шаблонных функций При перегрузке функций с шаблонами и без можно руководствоваться двумя правилами: 1. Точное соответствие лучше чем приведение типа 2. Частное лучше общего Пример: ```cpp= template<typename T, typename U> void f(T a, U b) { std::cout << 1 << '\n'; } template<typename T> void f(T a, T b) { std::cout << 2 << '\n'; } void f(int a, int b) { std::cout << 3 << '\n'; } int main() { f(1, 1.0f); // to call first function f(1.0f, 1.0f); // to call second function f(1, 1); // to call third function f<int>(0.0, 1); // call 2 f<int>(1, 1.0f); // call 1 f<int, double>(1, 1.0f); // call 1 f<int, double>(1.0f, 1); // call 1 f<>(1.0, 1); // to let compiler decide which version to use (only template functions) f(1.0, 1); // to let compiler decide which version to use (template and non-template variants) } ``` Пример 2: ```cpp= template<typename T> void f(T a) { std::cout << 1 << '\n'; } // if T already has & in type, no more & is added template<typename T> void f(T& a) { std::cout << 1 << '\n'; } int main() { f(1); // 1 int x = 0; f(x); // CE, ambiguous call f<int&>(x); // still CE } ``` **Примечание 1:** Для функции с несколькими шаблонными параметрами одного типа оба типа должны быть одинаковы при вызове этой функции. ### Шаблонные параметры по умолчанию Компилятор умеет сам выводить типы, которые надо подставить в шаблон. Но если их количество не совпадает с количеством при вызове, то это вызовет ошибку. ```cpp= template<typename T, typename U> void h(T x) { U y; std::cout << y << '\n'; } int main() { h(0); // CE } ``` Кроме того, можно указывать значения по умолчанию для шаблонных параметров. ```cpp= template<typename T, typename U = int> void h(T x) { U y; std::cout << y << '\n'; } int main() { h(0); // } ``` Вполне возможная ситуация: ```cpp= template<typename T> class A { public: A() = default; // common tempalte consructor template<typename U> A(U& a) { std::cout << 1 << '\n'; } // copy constructor A(const A<T>& other) { std::cout << 2 << '\n'; } }; int main() { A<int> s; A<int> ss = s; // 1 } ``` Такое происходит, так как первая версия предпочтительнее в силу того, что туда можно подставить любой тип. ## Шаблонные классы Кроме шаблонных функций можно создавать шаблонные классы. Пример шаблонного класса: ```cpp= template<typename T> class MyClass { // some code }; void foo() { MyClass<int> tmp; // create templated class // some code } ``` Кроме того, можно внутри шаблонного класса можно объявить шаблонный внутренний класс или функцию. Пример шаблонных классов: ```cpp= template<typename T> class A { public: T a; template<typename U> void foo(U arg1, T arg2) { // some code } template<typename U> // template inner class class Inner { U b; }; }; template<typename T, typename U> class S { T a; U b; }; ``` Если внутри шаблонного класса объявляется шаблонный класс или метод, то для его определения необходимо дважды указать ключевое слово ```template```. ```cpp= template<typename T> class A { private: T a; public: template<typename U> void foo(U arg1, T arg2); // declaration template<typename U> // template inner class class Inner { U b; }; }; template<typename T> template<typename U> void A<T>::foo(U arg1, T arg2) { // some code } ``` **Примечание 1:** Шаблон с несколькими аргументами не то же самое что шаблон внутри шаблона **Примечание 2:** При вызове шаблонной функции из шаблонного класса необязательно указывать шаблонные параметры явно. Пример: ```cpp= template<typename T> class A { private: T a; public: template<typename U> void foo(U arg1, T arg2); // declaration }; template<typename T> template<typename U> void A<T>::foo(U arg1, T arg2) { // some code } int main() { A<int> a; a.foo(1.5f, 1); // OK, first arg is float a.foo(1, "abc"); // CE, can't cast "abc" to int } ``` ### Шаблонные переменные С С++14 появилась возможность создавать шаблонные переменные Пример шаблонной переменной: ```cpp= template<typename T> double pi = 3.14; template<typename T> T pi = 3.14; ``` ### Шаблонные alias С С++11 есть возможность определять шаблонные ```alias```. Пример: ```cpp= template<typename T> using umap = std::unordered_map<T, T>; ``` ## Специализация шаблонов При написании шаблонов может возникнуть ситуация, когда надо написать некоторый обобщенный класс или функцию, но такую, чтобы для некоторых типов они работали чуть-чуть по-другому. В этом случае можно использовать частичную специализацию шаблонов. **Примечание 1:** Для функций возможна только полная специализация, т.е. версия функции с меньшим количеством шаблонных параметров считается новой, а не специализацией существующей. ### Специализация шаблонных классов Пример написания специализации шаблонного класса: ```cpp= template<typename T> class S { T x; }; template<> class S<int> { int x; }; ``` **Примечание 1:** Объявлять специализации можно только после объявления общей версии шаблона ### Специализация функций Шаблонные функции тоже можно специализировать ```cpp= template<typename T> void f(T x) { std::cout << 1 << '\n'; } template<> void f(int x) { std::cout << 2 << '\n'; } int main() { f(0); // 2 } ``` Однако, при добавлении обычной версии без шаблона с параметром ```int``` будет выбрана именно эта версия. ```cpp= template<typename T> void f(T x) { std::cout << 1 << '\n'; } template<> void f(int x) { std::cout << 2 << '\n'; } void f(int x) { std::cout << 3 << '\n'; } int main() { f(0); // 3 } ``` Тут срабатывает следующее правило: при подборе версий для вызова сначала выбирается та, которая лучше подходит, затем, если эта выбранная функция шаблонная, может быть выбрана её специализация. ### Порядок важен* При специализации версии специализаций подтягиваются к наилучшему сверху. Сравним следубщие примеры. Пример 1: ```cpp= template<typename T, typename U> void f(T x, U y) { std::cout << 1 << '\n'; } template<> void f(int x, int y) { std::cout << 2 << '\n'; } template<typename T> void f(T x, T y) { std::cout << 3 << '\n'; } int main() { f(0); // 3 } ``` Пример 2: ```cpp= template<typename T, typename U> void f(T x, U y) { std::cout << 1 << '\n'; } template<typename T> void f(T x, T y) { std::cout << 3 << '\n'; } template<> void f(int x, int y) { std::cout << 2 << '\n'; } int main() { f(0); // 2 } ``` Пример 3: ```cpp= template<typename T> void f(T x, T y) { std::cout << 3 << '\n'; } template<typename T, typename U> void f(T x, U y) { std::cout << 1 << '\n'; } template<> void f(int x, int y) { std::cout << 2 << '\n'; } int main() { f(0); // 2 } ``` ### Частичная специализация шаблонов В отличие от функций для классов возможна частичная специализация. ```cpp= template<typename T, typename U> class MyClass { public: void f() { std::cout << 1 << '\n'; } }; // partial specialization template<typename T, typename U> class MyClass<T&, U&> { public: void f() { std::cout << 2 << '\n'; } }; // partial specialization template<typename T> class MyClass<T, T> { public: void f() { std::cout << 3 << '\n'; } }; int main() { MyClass<int, double> s; s.f(); // 1 MyClass<int&, double&> ss; ss.f(); // 2 MyClass<int, int> sss; sss.f(); // 3 MyClass<int&, int&> ssss; ssss.f(); // CE } ``` ## Non-type шаблоны В качестве параметра шаблона могут выступать не только типы, но и определенные значения типов. Следующий пример демонстрирует *описание* класса массивов с использованием шаблонов: ```cpp= template<typename T, size_t N> class array { T a[N]; }; template<typename T, *T P> class MyClass { }; ``` Массивы одинаковых размеров складывать между собой мы можем и понимаем как это реализовывать. А вот операции с массивами разных размеров не поддерживаются. Мы не понимаем как работать с двумя *разными* типами. В качестве определенных значений можно почти все (из этого все уходят типы с плавающей точкой и еще некоторые). Параметр шаблона с определенным значением должен быть известен на этапе компиляции или же компилятор выдаст ошибку. ```cpp= #include <iostream> #include <array> int f(int x) { return x * x; } int main() { std::array<int, 5> arr0; // OK const int x = 5; std::array<int, x> arr1; // OK, the second parameter is known before compilation const int y = f(2); std::array<int, y> arr2; // CE, the second parameter need a calculation in run-time } ``` Значение шаблонного параметра должно быть зафиксировано в *момент компиляции*! ### Вычисления во время компиляции* Покажем на примере вычисления чисел Фибоначчи как можно использовать шаблонные классы ```cpp= #include <iostream> template<int N> struct Fibonacci { static const long long value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value }; template<> struct Fibonacci<1> { static const long long value = 1; }; template<> struct Fibonacci<0> { static const long long value = 0; }; int main() { std::cout << Fibonacci<23> << "\n"; return 0; } ``` Если попытаться скомпилировать код без специализации шаблонов для 0 и 1, то получим выход за максимальное количество шагов рекурсии. Это получается из-за того, что на этапе компиляции компилятор пытается вычислить значения чисел Фибоначчи для каждого N и переместить их в статическое поле памяти. Если же специализировать шаблоны начальными значениями, то программа скомпилируется и на этапе компиляции будут уже известны числа Фибоначчи Ограничение на шаблонную рекурсию можно изменить, если передать в компилятор флаг `-ftemplate-depth 100`. Это изменит глубину шаблонной рекурсии до 100 (по умолчанию 900). ## Шаблонные параметры шаблонов В качестве шаблонов могут выступать шаблоны. Синтаксис следующий: ```cpp= #include <iostream> #include <array> template<typename T, template<typename> class Container> struct Stack { Container<T> c; }; int main() { Stack<int, std::array> s; } ``` ## Инстанцирование классов Рассмотрим шаблонное наследование ```cpp= template<typename T> struct Base { int x; }; template<typename T> struct Derived: Base<T> { void f() { std::cout << x << '\n'; } }; ``` Данный код скомпилируется с ошибкой. Компилятор не понимает в какой именно из копии шаблонов нужно взять `x`. Для явного указания можно воспользоваться `this` или же `Base<T>::x` Компилятор собирает шаблонный код в два этапа: в момент *мета* обработки кода (проверка корректности выражений) и в момент подстановки шаблонных значений. ```cpp= template<typename T> struct Danger { int a[N]; }; template<typename T, int N> struct S { void f() { Danger<N> d; } void g() { Danger<-1> d; } }; ``` Если явно не объявить использование класса `S`, то компилятор не выдаст ошибки. С *мета* точки зрения код класса `S` является корректным как для шаблона. Если же начать объявлять такой объект (а точнее подставлять шаблонные значения для класса), то компилятор выдаст ошибку при попытке вызова метода `g()`. Пока метод не вызван, он **не истанцируется**. Ленивое инстанцирование шаблонов. При объявлении указателя на шаблонный класс (или же ссылки) компилятор лишь проверяет корректность переданных шаблонных параметров, но не инстанцирует поля и методы класса. Можно использовать объявленные шаблонные классы без их реализации для создания соответствующих указателей и ссылок. ```cpp= template<typename T, int N> // incomplete type struct S; int main() { S<int, -1>* ptr = nullptr; return 0; } ``` Так называемые неполные типы. ## Шаблоны с переменным количеством аргументов Шаблоны могут принимать переменное количество аргументов. Это позволяет написать свой собственный `print`. ```cpp= #include <iostream> template<typename Head> void print(const Head& head) { std::cout << head << "\n"; } template<typename Head, typename ...Tail> void print(const Head& head, Tail... tail) { std::cout << head << '\t'; print(tail...); } template<typename T, int N> // incomplete type struct S; int main() { print(12, "abc", "ichi"); return 0; } ``` В данном случае `Tail` является представителем переменного числа аргументов, пакет переменного числа параметров. Переменное число параметров распаковывается с помощью последующего троеточия. Размер пакета можно узнать с помощью `sizeof...()`. В качестве аргумента передается пакет (например, `Tail`). Рассмотрим еще один пример использование шаблонов для определения одинаковых типов: ```cpp= #include <iostream> template<typename T, typename U> struct is_same { static const bool value = false; }; template<typename T> struct is_same<T, T> { static const bool value = true; }; template<typename First, typename Second, typename ...Tail> struct is_homogeneous { static const bool value = std::is_same<First, Second>::value && is_homogeneous<Second, Tail...>::value; }; template<typename T, typename U> struct is_homogeneous<T, U> { static const bool value = is_same<T, U>::value; }; int main() { std::cout << is_homogeneous<int, int, int>::value << std::endl; std::cout << is_homogeneous<int, char, int>::value << std::endl; return 0; } ``` ## Задание Реализовать шаблонный класс матриц с операторами умножения, сложения, вычитания. Данные хранятся не в динамической памяти. *Шаблон* кода: ```cpp= template<typename Field, size_t M, size_t N> class Matrix { }; template<typename Field, size_t M, size_t N, size_t K> Matrix<Field, M, N> operator*(const Matrix<Field, M, N>& lha, const Matrix<Field, N, K>& rha) { } ``` Сохранять в `classwork05/matrix.cpp`