Шаблоны в C++ являются инструментами метапрограммирования и реализуют полиморфизм во время компиляции.
Что это? Это когда мы пишем код с полиморфным поведением, но само поведение определяется на этапе компиляции — то есть, в отличие от полиморфизма виртуальных функций, результирующий двоичный код уже будет иметь постоянное поведение.
За что?
Используем шаблоны для красоты.
Каждый разработчик C++ знает, что такое красота; красота - это когда код компактный , понятно и быстро .
Мета-магия и неявные интерфейсы
Что такое метапрограмма? Метапрограмма — это программа, результатом работы которой станет другая программа.В C++ компилятор выполняет метапрограммы, и результатом является двоичный файл.
Шаблоны используются для написания метапрограмм.
Чем еще полиморфизм шаблонов отличается от полиморфизма виртуальных функций? Если класс имеет явный интерфейс, который мы определили в объявлении класса, то в дальнейшем в программе объекты этого типа могут использоваться в соответствии с этим самым интерфейсом.
Но для шаблонов мы используем неявные интерфейсы, т.е.
используя объект типа, который мы определяем интерфейс неявного типа , который компилятор выведет при построении метапрограммы.
Первые заклинания: волшебная дубина
Давайте усовершенствуем наш шаблон и посмотрим, какие типы мы получили для различных параметров шаблона:
Вывод программы показывает, что типы экземпляров шаблонов разные даже для эквивалентных типов — unsigned char & char. Более того, они идентичны для char и CHAR, поскольку typedef не создает тип, а просто присваивает ему другое имя.typedef char CHAR; int main() { B<int> b; B<char> c; B<unsigned char> uc; B<signed char> sc; B<CHAR> C; B<char, 1> c1; B<char, 2-1> c21; cout << "b=" << typeid(b).
name() << endl; cout << "c=" << typeid(c).
name() << endl; cout << "C=" << typeid(C).
name() << endl; cout << "sc=" << typeid(sc).
name() << endl; cout << "uc=" << typeid(uc).
name() << endl; cout << "c1=" << typeid(c1).
name() << endl; cout << "c21=" << typeid(c21).
name() << endl; return 0; }
Они идентичны для выражений 1 и 2-1, поскольку компилятор оценивает выражения и использует 1 вместо 2-1.
Это значит, что мы не можем без дополнительных проблем использовать отдельную компиляцию для шаблонов: ааа #include <iostream>
using namespace std;
template <typename T> class A {
public:
void f();
};
main.cpp #include "export.h"
int main() {
A<int> a;
a.f();
return 0;
}
.
cpp #include "a.h"
template <typename T> void A<T>::f() {
cout << "A<t>::f" << endl;
}
template class A<int>;
В общем, в стандарте C++ для этого есть кое-что: ключевое слово экспорта , однако эта возможность слишком сложна в реализации и отсутствует в большинстве компиляторов.
Существуют компиляторы, которые его поддерживают, но я не рекомендую использовать его в переносимом коде.
Помимо классов существуют еще шаблоны функций: template<typename T> T func(T t, T d) {
cout << "func" << endl;
};
int main() {
func('1', 2);
}
Если компилятор может определить тип параметра шаблона по типу параметров, он так и сделает, и нам не нужно указывать его в коде.
Если нет, то мы можем определить разрешающую функцию: inline int func(char c, int i) {
return func<int>(c, i);
};
Это не влечет за собой никаких накладных расходов.
Специализация – новый уровень
Обычно с помощью шаблонов мы хотим написать универсальный код, но в некоторых случаях мы можем потерять в производительности.
Для решения проблемы существует специальное заклинание – шаблонная специализация.
Специализация — это переопределение шаблона с определенным типом или тип класса : #include <iostream>
using namespace std;
template<typename T> T func(T t) {
cout << "func" << endl;
};
template<typename T> T * func(T *t) {
cout << "func with pointer!" << endl;
};
int main() {
func(2);
int i = 2;
func(&i);
}
Компилятор сам выберет наиболее точно подходящую специализацию, в примере это класс типа «указатель на тип».
Зловещая магия: Рекурсия
Специализация и тот факт, что мы можем использовать шаблоны внутри шаблонов, дают нам одну очень интересную особенность — рекурсию времени компиляции.
Самый простой и популярный пример — вычисление какого-нибудь ряда или многочлена, скажем, суммы ряда натуральных чисел: #include <iostream>
using namespace std;
template <int i> int func() {
return func<i-1>()+i;
};
template <> int func<0>() {
return 0;
};
int main () {
cout << func<12>() << endl;
return 0;
};
Посмотрим.
Это работает! Прохладный? Увеличим количество итераций до 500: cout << func<500>() << endl;
Теперь компиляция занимает больше времени, но время выполнения программы остается постоянным! Чудеса!
Не делай козла, если хочешь грозу
Здесь есть пара моментов.
Максимальная глубина рекурсии по умолчанию ограничена реализацией, для нового gcc она равна 900, для старых версий меньше.
Параметр $ g++ recursion.cpp -ftemplate-depth=666666666
снимает это ограничение.
Вторая ловушка — не ждите отчетов об ошибках.
Измените сумму на факториал: int func() {
return func<i-1>() * i;
};
template <> int func<0>() {
return 1;
};
.
cout << func<500>() << endl;
Получаем неверный результат и ни одного предупреждения.
Третий момент очевиден: мы можем создать слишком много почти одинаковых экземпляров шаблона и Вместо прироста производительности получите прирост бинарного кода .
Мощные заклинания древних
Можно ли совместить магию наследования с магией шаблонов?Древние для этого использовали заклинание ЦРТП .
Идея проста: использовать невиртуальное наследование и обеспечивать полиморфное поведение, явно приводя тип наследника к типу родителя.
Давайте рассмотрим пример использования : template<typename Filtrator> class FiltratorImpl {
inline void find_message(.
) { Filtrator* filtrator = static_cast<Filtrator* >(this); … filtrator->find_and_read_message(info, collection); } }; .
class CIFSFiltrator : public FiltratorImpl<CIFSFiltrator> { .
inline void find_and_read_message(PacketInfo& info) {.
} .
}; class RPCFiltrator : public FiltratorImpl<RPCFiltrator> { .
inline void find_and_read_message(PacketInfo& info) {.
} .
};
Мы получаем унаследованные встроенные методы с полиморфным поведением! Кто говорит, что это не круто, тот мой враг навсегда.
Еще древние советуют добавлять в родительский конструктор что-то вроде этого: static_assert(std::is_member_function_pointer<decltype(&Filtrator::find_and_read_message)>::value)
Чтобы демоны, разбуженные мощным заклинанием, не смогли навредить магу, их вызвавшему.
Есть еще много секретных техник, древних и не очень.
Надеюсь скоро увидеть тебя /*в аду*/, и да пребудет с тобой сила древних.
Теги: #шаблон #производительность #C++
-
Nb-Iot: Как Это Работает? Часть 1
19 Oct, 24 -
Второй Hdd Вместо Dvd-Привода Для Ноутбука
19 Oct, 24 -
Примечание Для Пользователей Яндекс.денег
19 Oct, 24 -
Двигатель Как Электромагнитный Тормоз
19 Oct, 24 -
Жизнь Проектов После Смерти Создателя
19 Oct, 24