Язык С++. Шаблоны.


Шаблоны (англ. template) - средство языка C++, предназначенное для кодирования обобщённых алгоритмов, без привязки к некоторым параметрам (например, типам данных, размерам буферов, значениям по умолчанию).
Шаблоны являются пожалуй, одной самой сложной для изучения частью языка С++.
Основные аргументы сложности шаблонов:
  • Шаблоны трудно изучить, понять, и адаптировать к применению.
  • Сообщения об ошибках компилятора, связанные с шаблонами, часто весьма непонятны, и очень большие по размеру.
Изучение шаблонов достаточно трудоемкий процесс, но и их использование дает массу преимуществ.
Обобщенное программирование - это создание кода, работающего с разными типами, заданными в виде аргументов, причем эти типы должны соответствовать специфическим синтаксическим и семантическим требованиям. Форма обобщенного программирования, основанная на шаблонных параметрах, часто называются параметрическим полиморфизмом, в отличии от обычного полиморфизма на основе иерархий классов и виртуальных функций. Причина, по которой оба стиля программирования называются полиморфизмом, заключается в том, что каждый из них дает программисту возможность создавать много версий  одной и той же концепции с помощью единого интерфейса. Имеется ряд отличий объектно-ориентированного программирования (с помощью иерархий классов и виртуальных функций) от обобщенного программирования (с помощью шаблонов), что позволяет говорить о полиморфизмах времени компиляции и времени выполнения, или о статическом(шаблоны) и динамическом полиморфизмах(виртуальные функции).
  v.push_back(x); // внести x в вектор
  s.draw();       // нарисовать фигуру s
В вызове v.push_back(x) компилятор определит тип элементов в объекте v и применит соответствующую функцию push_back(), а для вызова s.draw() он косвенно(с помощью таблицы виртуальных функций, связанной с объектом s) вызовет функцию draw(). Это дает объектно-ориентированному программированию ту степень свободы, которой лишено обобщенное программирование, но в то же время делает обычное обобщенное программирование более систематическим, понятным и эффективным.
Используйте шаблоны, когда важную роль играет производительность программы.
Используйте шаблоны, когда важна гибкость сочетания информации из разных типов.

Шаблоны обладают многими полезными свойствами, такими как высокая гибкость и почти оптимальная производительность, но и они не идеальны. Как всегда, преимуществам сопутствуют недостатки. Основным недостатком шаблонов является то, что гибкость и высокая производительность достигаются за счет плохого отделения реализации(определение) от его интерфейса(объявление). Это проявляется в плохой диагностике ошибок, особенно жуткими являются сообщения об ошибках.
При компиляции программы, использующей шаблоны, компилятор "заглядывает" внутрь шаблонов и их аргументов. Он делает это для того, чтобы извлечь информацию, необходимую для генерации оптимального кода. Чтобы эта информация стала доступной, современные компиляторы требуют, чтобы шаблон был полностью определен везде, где он используется. Это относится и к его функциям-членам и ко всем шаблонным функциям, вызываемым из них. В результате авторы шаблонов стараются разместить определения шаблонов в заголовочных файлах. На самом деле стандарт этого не требует, но пока не будут разработаны существенно более эффективные реализации языка, рекомендуется поступать со своими шаблонами именно так - размещайте в заголовочных файлах определения всех шаблонов, используемых в нескольких единицах трансляции.
С шаблонами программист может уменьшить количество написанного кода, если нужно реализовать аналогичный функционал для различных исходных типов. Например, есть функция, и она должна работать для параметров разного типа. Конечно, можно написать несколько разных функций, или воспользоваться перезагрузкой функций, но шаблоны предоставляют альтернативный путь. Т. е. для функции (или класса) имеется некий формализованный код (шаблон), в который передаются типы, и компилятор на основе этого сам строит рабочий код.
Давайте приступим к примерам.
К примеру стоит задача определить максимальное значение, из двух объектов одинаковых типов. Типы могу быть разные. Рассмотрим как бы мы это решали на языке Си(это один из вариантом, которые не совершенен, но очень хорошо подходит для понимания шаблонной функции). Решением может быть следующим, создать под каждый тип данных свою функцию.
int max_i(int a, int b)
{
  return a > b ? a : b;
}
int max_f(float a, float b)
{
  return a > b ? a : b;
}
int max_uch(unsigned char a, unsigned char b)
{
  return a > b ? a : b;
}
Конечно это не снимает проблемы передачи в качестве аргументов объектов различных типов, но мы уже описали 3 функции которые сравнивают объекты конкретных типов. Для добавления новых типов, нужны новые функции, чтобы избежать проблем с конвертацией одних типов в другие, эти проблемы ведут к неопределенному поведению программы. 
Это хорошо, но было бы лучше если бы компилятор смог бы сам генерировать нужные нам функции, то есть неплохо было бы если бы компилятор анализировал код и перед компиляцией определял объекты каких типов учавствуют в операциях сравнения и генерил для этих типов собственные функции. Попробуем улучшить код выше с использованием макросов.
#define template_max(name, type) int max##name(type a, type b) \
{\
return a > b ? a : b;\
}

template_max(Int, int);
template_max(Float, float);
template_max(UChar, unsigned char);

int main()
{
  maxFloat(1.f, 2.f);
  maxInt(1, 2);
  maxUChar(2,3);
  return 0;
}
Уже лучше, кода уже меньше, для того чтобы указать компилятору какие функции и с какими типами нам нужны, перед функцией main() мы это указали, и после работы препроцессора на вход компилятору уйдет следующий код
int maxInt(int a, int b) {return a > b ? a : b;};
int maxFloat(float a, float b) {return a > b ? a : b;};
int maxUChar(unsigned char a, unsigned char b) {return a > b ? a : b;};

int main()
{
  maxFloat(1.f, 2.f);
  maxInt(1, 2);
  maxUChar(2,3);
  return 0;
}
В котором как мы видим уже сгенерены нужные нам функции.
Но попрежнему это все не гарантирует нам того что мы не сможем случайно передать в качестве аргументов объекты с разными типами, и получить урезание типа и как результат некоректную работу программы.
Мы уже почти подошли к пониманию того что такое шаблонная функция в самом простом понимании. Рассмотрим пример ниже.
template<typename T>
T max(T a, T b)
{
  return a > b ? a : b;
}

int main()
{
  int ai = 0;
  int bi = 1;

  float af = 90;
  float bf = 1;

  max(ai, bi);          // жесткая типизация
  max(af, bf);          // жесткая типизация

  //max(ai, bf);        // Ошибка! в аргументах разные типы
                        // в шаблоне указан только один тип

  //max('0', 0);        // Ошибка! в аргументах разные типы
                        // в шаблоне указан только один тип

  max<int>('1',  bf);   // все аргументы будут приведены к типу int
  max<double>(1, af);   // все аргументы будут приведены к типу double

  return 0;
}
В самом начале мы видим шаблонную функцию, max(), слово template указывает что это шаблон, typename указывает что там будет некий тип с псевдонимом T, этот тип будет известен на этапе компиляции. Функция возвращает данные того же типа, какого параметры функции. На этапе сборки, компилятор видит использование шаблонной функции в коде с аргументами двух типов(int, float) по аналогии с предыдущем примером на языке Си, в этом случае генерятся 2 функции с этими типами. То есть генерятся только функции с теми типами, которые реально используются в коде. Если мы попытаемся передать 2 объекта с разными типами, то получим ошибку сборки. Так как в правилах шаблонной функции мы указали использование только одного типа T. Для передачи объектов различных типов, мы можем использовать max<int>('1', bf); в треугльных скобках мы указывает тип данных, к которому должны будут приведены все аргументы. Соответственно случайно передать объекты с разными типами тоже не получится, так как мы четко видим тип и прекрасно понимаем что все аргументы будут приведены к типу int.

Специализированные, частично специализированные шаблонные функции.
Шаблоны функций можно настроить для определенных типов или значений аргументов шаблона.Специализация позволяет выполнять настройку кода шаблона для конкретного типа аргумента или значения. Без специализации один и тот же код создается для всех типов, используемых в создании экземпляра шаблонной функции. В специализации, когда используются определенные типы, вместо исходного определения шаблона используется определение для специализации. Используя шаблон функции, можно указать особое поведение для определенного типа, предоставив явную специализацию (переопределение) шаблона функции для этого типа.
#include <iostream>
template<typename T> void print_info(T aValue)
{
  std::cout << aValue << std::endl;
}
int main()
{
  print_info(3.14159);// PI
  print_info(1.1f);   // a coefficient
  print_info('S');    // direction - South
  print_info("Hello");// message
  return 0;
}
В примере выше мы создали шаблонную функцию для вывода некой информации на консоль(терминал). Мы можем сделать частичную специализацию, в которой можем указать вторым обязательным аргументом указать информацию о выводимом поле, смотрим пример ниже
#include <iostream>
#include <string>
template<typename T> void print_info(T aValue, std::string aInfo)
{
  std::cout << aInfo << " : " << aValue << std::endl;
}
int main()
{
  print_info(3.14159, "PI");
  print_info(1.1f, "Coof");
  print_info('S', "Direction");
  print_info("Hello", "Message");
  return 0;
}
В этом примере мы создали частично специализированную функцию, первым параметром тип будет определен на этапе сборки, а вторым параметром является строковая переменная, которая дает информацию о выводимой информации.
Мы можем создать полную специализацию, в которой можем указать конкретный тип с которым мы будем работать. Рассмотрим пример ниже, где показан один из примеров того, что общая шаблонная функция не подходит для всех типов.
#include <iostream>
#include <string>

struct info
{
  float       coof{1.1f};
  char        direction{'S'};
  std::string message{"Hello"};
};

template<typename T> void print_info(T aValue, std::string aInfo)
{
  std::cout << aInfo << " : " << aValue << std::endl;
}

int main()
{
  info *f = new info;
  print_info(f, "info");
  return 0;
}
В этом примере, так уж получилось мы передали в функцию указатель на сложный тип, в результате на экране будет выведен адрес этого объекта, но совсем не то что мы ожидали. Мы можем создать полную специализацию, и описать реализацию для конкретного типа
#include <iostream>
#include <string>

struct info
{
  float       coof{1.1f};
  char        direction{'S'};
  std::string message{"Hello"};
};

template<typename T> void print_info(T aValue, std::string aInfo)
{
  std::cout << aInfo << " : " << aValue << std::endl;
}

template<> void print_info<info*>(info *aValue, std::string aInfo)
{
  std::cout << aInfo << std::endl;
  std::cout << "Coof      : " << aValue->coof << std::endl;
  std::cout << "Direction : " << aValue->direction << std::endl;
  std::cout << "Message   : " << aValue->message << std::endl;
}

int main()
{
  info *f = new info;
  print_info(f, "info");
  return 0;
}
В этом примере мы создали шаблонную функцию в которой указали конкретный тип(указатель на объект типа info), объекты которого мы обрабатываем. Это называется полной специализацией. При создании шаблонной функции, в треугольных скобках template<> мы ничего не указали, а указали это после имени функции print_info<info*>. 
Кроме всего прочего допускается перегрузка шаблонных функций, по аналогии с обычными функции.



!!!ДОПОЛНЯЕТСЯ!!!














Комментарии