Шаблоны функций c. Шаблоны
Шаблоны функций, своими словами,- это инструкции, согласно которым создаются локальные версии шаблонированной функции для определенного набора параметров и типов данных.
На самом деле, шаблоны функций -это мощный инструмент в С++, который намного упрощает труд программиста. Например, нам нужно запрограммировать функцию, которая выводила бы на экран элементы массива. Задача не сложная! Но, чтобы написать такую функцию, мы должны знать тип данных массива, который будем выводить на экран. И тут нам говорят — тип данных не один, мы хотим, чтобы функция выводила массивы типа int , double , float и char .
Как оказалось, задача усложнилась. И теперь мы понимаем, что нам нужно запрограммировать целых 4 функции, которые выполняют одни и те же действия, но для различных типов данных. Так как мы еще не знакомы с шаблонами функций, мы поступим так: воспользуемся .
// перегрузка функции printArray для вывода массива на экран void printArray(const int * array, int count) { for (int ix = 0; ix < count; ix++) cout << array << " "; cout << endl; } void printArray(const double * array, int count) { for (int ix = 0; ix < count; ix++) cout << array << " "; cout << endl; } void printArray(const float * array, int count) { for (int ix = 0; ix < count; ix++) cout << array << " "; cout << endl; } void printArray(const char * array, int count) { for (int ix = 0; ix < count; ix++) cout << array << " "; cout << endl; }
Таким образом, мы имеем 4 перегруженные функции, для разных типов данных. Как видите, они отличаются только заголовком функции, тело у них абсолютно одинаковое. Я написал один раз тело функции для типа int и три раза его скопировал для других типов данных.
И, если запустить программу с этими функциями, то она будет исправно работать. Компилятор сам будет определять какую функцию использовать при вызове.
Как видите, кода получилось достаточно много, как для такой простой операции. А что если, нам понадобится запрограммировать в виде функции. Получается, что для каждого типа данных придется свою функцию создавать. То есть, сами понимаете, что один и тот же код будет в нескольких экземплярах, нам это ни к чему. Поэтому в С++ придуман такой механизм - шаблоны функций.
Мы создаем один шаблон, в котором описываем все типы данных. Таким образом исходник не будет захламляться никому ненужными строками кода. Ниже рассмотрим пример программы с шаблоном функции. Итак, вспомним условие: «запрограммировать функцию, которая выводила бы на экран элементы массива».
// код Code::Blocks
// код Dev-C++
#include
Заметьте, код уменьшился в 4 раза, так как в программе объявлен всего один экземпляр функции - шаблон. В main я объявил несколько массивов - четыре, для типов данных: int , double , float , char . После чего, в строках 26, 28, 30, 32, выполняется вызов функции printArray для разных массивов. Результат работы программы показан ниже.
Шаблон функции вывода массива на экран Массив типа int: 1 2 3 4 5 6 7 8 9 10 Массив типа double: 1.2345 2.234 3.57 4.67876 5.346 6.1545 7.7682 Массив типа float: 1.34 2.37 3.23 4.8 5.879 6.345 73.434 8.82 9.33 10.4 Массив типа char: M A R S
Как видите программа корректно работает, и для этого нам понадобилось всего один раз определить функцию printArray в привычном для нас виде. Обратите внимание, что перед объявлением самой функции, в строке 5, стоит следующая запись template
Все шаблоны функций начинаются со слова template , после которого идут угловые скобки, в которых перечисляется список параметров. Каждому параметру должно предшествовать зарезервированное слово class или typename .
Template
Template
Template
Ключевое слово typename говорит о том, что в шаблоне будет использоваться встроенный тип данных, такой как: int , double , float , char и т. д. А ключевое слово class сообщает компилятору, что в шаблоне функции в качестве параметра будут использоваться пользовательские типы данных, то есть классы.
У нас в шаблоне функции использовались встроенные типы данных, поэтому в строке 5 мы написали template
// шаблон функции printArray
template
В строке 2 выполняется определение шаблона с одним параметром - T , причем этот параметр будет иметь один из встроенных типов данных, так как указано ключевое слово typename .
Ниже, в строках 3 — 8 объявлена функция, которая соответствует всем критериям объявления обычной функции, есть заголовок, есть тело функции, в заголовке есть имя и параметры функции, все как обычно. Но что эту функции превращает в шаблон функции, так это параметр с типом данных T , это единственная связь с шаблоном, объявленным ранее. Если бы мы написали
Void printArray(const int * array, int count) { for (int ix = 0; ix < count; ix++) cout << array << " "; cout << endl; }
то это была бы простая функция для массива типа int .
Так вот, по сути T - это даже не тип данных, это зарезервированное место под любой встроенный тип данных. То есть когда выполняется вызов этой функции, компилятор анализирует параметр шаблонированной функции и создает экземпляр для соответственного типа данных: int , char и так далее.
Поэтому следует понимать, что даже если объем кода меньше, то это не значит, что памяти программа будет потреблять меньше. Компилятор сам создает локальные копии функции-шаблона и соответственно памяти потребляется столько, как если бы вы сами написали все экземпляры функции, как в случае с перегрузкой.
Надеюсь основную мысль по шаблонам функций до вас довел. Для закрепления материала, давайте рассмотрим еще один пример программы, с использованием шаблона функции.
#include "stdafx.h"
#include
// код Code::Blocks
// код Dev-C++
#include
Вот вам еще один пример использования шаблонов функций. Шаблон функции объявлен в строках 5-13. Функция должна возвращать максимальное значение массива, поэтому возвращаемое значение типа T , ведь тип данных массива заранее не известен. Кстати внутри функции объявлена переменная max типа T , в ней будет храниться максимальное значение массива. Как видите, тип данных T используется не только для спецификации параметров функции, но и для указания типа возвращаемого значения, а также может свободно использоваться для объявления любых переменных внутри шаблона функции.
Максимальный элемент массива типа char: s Максимальный элемент массива типа int: 9
Шаблоны функций также можно перегружать другими шаблонами функций, изменив количество передаваемых параметров в функцию. Еще одной особенностью перегрузки является то, что шаблонные функции могут быть перегружены обычными не шаблонными функциями. То есть указывается то же самое имя функции, с теми же параметрами, но для определенного типа данных, и все будет корректно работать.
Собрался я было писать текст про всякие крутые структуры данных и тут оказалось, что мы ещё не разбирали несколько очень важных возможностей C++. Шаблоны - одна из них.
Шаблоны (templates) - очень мощное средство. Шаблонные функции и классы позволяют очень сильно упростить программисту жизнь и сберечь огромное количество времени, сил и нервов. Если вам покажется, что шаблоны не сильно-то и значимая тема для изучения, знайте - вы заблуждаетесь.
Шаблонные функции
Простой пример шаблонной функции:
код на языке c++ Type square (Type a) { Type b; b = a*a; return b; } int x = 5; int i; i = square(5); float y = 0.5; float f; f = square(y);
Если бы мы создавали функции по старинке, то тогда бы пришлось писать две разные функции: для типа int и для типа float. А если бы понадобилась такая же функция, использующая другие типы, пришлось бы заново писать и её. Используя шаблоны, можно ограничиться только одним экземпляром функции, оставив всю грязную работу компилятору.
Вместо использования какого-то определённого типа, в функции используется параметрический тип (или по другому - аргумент шаблона). Здесь я обозвал параметрический тип идентификатором Type. В функции этот идентификатор встречается три раза: возвращаемое значение, аргумент функции и определение переменной s. То есть Type используется как любой обычный тип.
Но чтобы код заработал, перед функцией нужно добавить следующую строку (я показал несколько вариантов синтаксиса, все они рабочие):
код на языке c++
template
Итак, перед функцией должно стоять ключевое слово template (шаблон), а в угловых скобках нужно указать имя параметрического типа с ключевым словом class. Вместо ключевого слова class можно использовать type - в общем-то никакой разницы.
Идентификатор параметрического типа тоже может быть любым. Мы часто будем пользоваться вот такими: TypeA, TypeB, Datatype, T.
Важное замечание : У шаблонных функций должен быть аргумент, чтобы компилятор мог определить какой именно тип использовать.
В шаблонах можно использовать несколько параметрических типов, и конечно же можно смешивать параметрические типы со стандартными (только нужно позаботиться о правильном приведении типов). Приведу пример в котором используется два параметрических типа TypeA, TypeB и базовый тип int:
код на языке c++
template
Но шаблонные функции - не самое интересное, что мы сегодня рассмотрим.
Шаблонные классы
В общем-то шаблонные классы создаются почти так же как и шаблонные функции - перед именем класса записывается ключевое слово template. Шаблонные классы рассмотрим на примере стека:
код на языке c++
template
делили стек из десяти элементов. Эти элементы могут быть какого угодно типа, об этом чуть-чуть ниже.
Единственное на что хочу обратить ваше внимание: определение функций push и pop. Функция push определена внутри класса, а функция pop - снаружи. Для всех функции объявлённых за пределами класса, нужно обязательно указывать ключевое слово template. Выражение перед именем функции совпадает с тем, которое указывается перед именем класса.
Теперь посмотрим как работать с шаблонными классами:
код на языке c++
stack
При создании объекта, после имени класса нужно поставить угловые скобки, в которых указать нужный тип. После этого объекты используются так, как мы привыкли.
У шаблонных классов есть одна потрясающая особенность - кроме стандартных типов, они могут работать и с пользовательскими. Рассмотрим небольшой пример. Для этого определим простой класс warrior:
код на языке c++
class warrior
{
public:
int health;
warrior () : health(0) {}
};
stack
Смотрите, теперь в стеках можно размещать переменные типа warrior!!! Возможно вы не поверите мне, но это очень круто! Насколько это круто, вы сможете убедиться когда на основе списков мы будем создавать графы и деревья.
По шаблонам пока всё. Позже разберём более сложные случаи использования шаблонных классов.
До настоящего момента обсуждались вопросы построения шаблонных классов на основе предопределенных (включенных в состав библиотеки классов) шаблонов классов. В этом разделе на простом примере обсуждается техника объявления собственных шаблонов классов и шаблонов функций.
Шаблонный класс обеспечивает стандартную реализацию дополнительной функциональности на основе ранее объявленных подстановочных классов.
Эта дополнительная функциональность может накладывать дополнительные ограничения на подстановочный класс . Например, для успешной работы объекта шаблонного класса подстановочный класс должен наследовать определенному интерфейсу. Иначе функциональность шаблонного класса просто невозможно будет реализовать.
Для формирования ограничений на подстановочные классы в C# используется механизм ограничителей параметров шаблона - он вводится при объявлении шаблона с помощью ключевого слова where , за которым могут располагаться имя параметра типа и список типов класса или интерфейса либо конструктор – ограничение new() :
Using System;
using System.Collections;
using System.Collections.Generic;
namespace PatternArrays
{
//========== Это заголовок шаблона класса W ==========
// Шаблон класса своими руками. T – параметр шаблона.
// Шаблонный класс – это класс-шаблон, который детализируется
// подстановочным классом.
// При создании шаблонного класса вхождения параметра шаблона
// (в данном случае это T) замещаются именем подстановочного
// класса. Разработчик шаблона класса может выдвигать требования
// относительно характеристик подстановочного класса.
// Для этого используются специальные языковые конструкции,
// называемые ОГРАНИЧИТЕЛЯМИ ПАРАМЕТРА ШАБЛОНА.
// ОГРАНИЧИТЕЛЬ ПАРАМЕТРА ШАБЛОНА формулирует требования для
// подстановочного класса.
class W
Пример использования шаблонов: сортировка
Старая задача, новые решения с использованием предопределенных шаблонов классов и интерфейсов...
Using System;
using System.Collections;
using System.Collections.Generic;
namespace PatternArrays
{
// Данные для массива элементов.
// Подлежат сортировке в составе шаблонного массива методом Sort.
class Points
{
public int x;
public int y;
public Points(int key1, int key2)
{
x = key1;
y = key2;
}
// Вычисляется расстояние от начала координат.
public int R
{
get
{
return (int)(Math.Sqrt(x * x + y * y));
}
}
}
// ...ШАБЛОННЫЙ КОМПАРЕР на основе шаблона интерфейса...
class myComparer: IComparer
Nullable-типы
Nullable-типы (простые Nullable-типы) представляют собой расширения простых типов. Их объявления принадлежат пространству имен System.Nullable .
Это шаблонные типы, то есть типы, построенные в результате детализации шаблонов. Шаблон Nullable<> используется для расширения простых типов, которые по своей сути являются структурами. Для обозначения Nullable шаблонных (построенных на основе шаблона) типов используются две нотации.
Специализация шаблонов является одной из «сложных» фичей языка с++ и использутся в основном при создании библиотек. К сожалению, некоторые особенности специализации шаблонов не очень хорошо раскрыты в популярных книгах по этому языку. Более того, даже 53 страницы официального ISO стандарта языка, посвященные шаблонам, описывают интересные детали сумбурно, оставляя многое на «догадайтесь сами - это же очевидно». Под катом я постарался ясно изложить базовые принципы специализации шаблонов и показать как эти принципы можно использовать в построении магических заклинаний.
Hello World
Как мы привыкли использовать шаблоны? Используем ключевое слово template, затем в угловых скобках имена параметров шаблона , после чего тип и имя. Для параметров также указывают что это такое: тип (typename) или значение (например, int). Тип самого шаблона может быть класс (class), структура (struct - вообщем-то тоже класс) или функция (bool foo() и так далее). Например, простейший шаблонный класс "A" можно задать вот так:Через некоторое время мы захотим, чтобы наш класс для всех типов работал одинаково, а для какого-нибудь хитрого вроде int - по-другому. Фигня вопрос, пишем специализацию: выглядит так же как объявление но параметры шаблона в угловых скобках не указываем, вместо этого указываем конкретные аргументы шаблона после его имени:
Template<> class A< int > {}; // здесь int - это аргумент шаблона
Готово, можно писать методы и поля специальной реализации для int. Такая специализация обычно называется полной
(full specialization или explicit specialization). Для большинства практических задач большего не требуется. А если требуется, то…
Специализированный шаблон - это новый шаблон
Если внимательно читать ISO стандарт С++, то можно обнаружить интересное утверждение: создав специализированный шаблонный класс мы создаем новый шаблонный класс (14.5.4.3). Что это нам дает? Специализированный шаблонный класс может содержать методы, поля или объявления типов которых нет в шаблонном классе который мы специализируем. Удобно, когда нужно чтобы метод шаблонного класса работал только для конкретной специализации - достаточно объявить метод только в этой специализации, остальное сделает компилятор:Специализированный шаблон может иметь свои параметры шаблона
Дьявол, как известно, в деталях. То, что специализированный шаблонный класс это совсем-совсем новый и отдельный класс конечно интересно, но магии в этом мало. А магия есть в незначительном следствии - если это отдельный шаблонный класс, то он может иметь отдельные, никак не связанные с неспециализированным шаблонным классом параметры (параметры - это то, что после template в угловых скобках). Например, вот так:Template< typename S, typename U > class A< int > {};
Правда, именно такой код компилятор не скомпилирует
- новые параметры шаблона S и U мы никак не используем, что для специализированного шаблонного класса запрещено (а то что это класс специализированный компилятор понимает потому, что у него такое же имя "A" как у уже объявленного шаблонного класса). Компилятор даже специальную ошибку скажет: «explicit specialization is using partial specialization syntax, use template<> instead». Намекает, что если сказать нечего - то надо использовать template<> и не выпендриваться. Тогда для чего же в специализированном шаблонном классе можно использовать новые параметры? Ответ странный - для того, чтобы задать аргументы
специализации (аргументы - это то, что после имени класса в угловых скобках). То есть специализируя шаблонный класс мы можем вместо простого и понятного int специализировать его через новые параметры
:
Template< typename S, typename U > class A< std::map< S, U > > {};
Такая странная запись скомпилируется. И при использовании получившегося шаблонного класса с std::map будет использована специализация, где тип ключа std::map будет доступен как параметр нового шаблона S, а тип значения std::map как U.
Такая специализация шаблона, при которой задается новый список параметров и через эти параметры задаются аргументы для специализации называется частичной специализацией (partial specialization). Почему «частичной»? Видимо потому, что изначально задумывалась как синтаксис для специализации шаблона не по всем аргументам. Пример, где шаблонный класс с двумя параметрами специализируется только по одному из них (специализация будет работать когда первый аргумент, T, будет указан как int. При этом второй аргумент может быть любым - для этого в частичной специализации введен новый параметр U и указан в списке аргументов для специализации):
Template< typename T, typename S > class B {}; template< typename U > class B< int, U > {};
Магические последствия частичной специализации
Из двух вышеописанных свойств специализации шаблонов есть ряд интересных следствий. Например, при использовании частичной специализации можно, вводя новые параметры шаблона и описывая через них специализированные аргументы, разбивать составные типы на простейшие. В приведенном ниже примере специализированный шаблонный класс A будет использован, если аргументов шаблона является тип указателя на функцию. При этом через новые параметры шаблона S и U можно получить тип возвращаемого значения этой функции и тип ее аргумента:Template< typename S, typename U > class A< S(*)(U) > {};
А если в специализированном шаблоне объявить typedef или static const int (пользуясь тем, что это новый шаблон), то можно использовать его для извлечения нужной информации из типа. Например, мы используем шаблонный класс для хранения объектов и хотим получить размер переданного объекта или 0, если это указатель. В две строчки:
Template< typename T > struct Get { const static int Size = sizeof(T); };
template< typename S > struct Get< S* > { const static int Size = 0; };
Get< int >::Size // например, 4
Get< int* >::Size // 0 - нашли указатель:)
Магия этого типа используется в основном в библиотеках: stl, boost, loki и так далее. Конечно, при высокоуровневом программировании использовать такие фокусы череповато - думаю, все помнят конструкцию для получения размера массива:). Но в библиотеках частичная специализация позволяет относительно просто реализовывать делегаты, события, сложные контейнеры и прочие иногда очень нужные и полезные вещи.
Коллеги, если найдете ошибку (а я, к сожалению, не гуру - могу ошибаться) или у Вас есть критика, вопросы али дополнения к вышеизложенному - буду рад комментариям.
Update: Обещанное продолжение
Функция-шаблон определяет общий набор операций, который будет применен к данным различных типов. Используя этот механизм, можно применять некоторые общие алгоритмы к широкому кругу данных. Как известно, многие алгоритмы логически одинаковы вне зависимости от типа данных, с которыми они оперируют. Например, алгоритм быстрой сортировки Quicksort один и тот же и для массива целых чисел, и для массива чисел с плавающей запятой. Отличается только тип данных, подлежащих сортировке. При помощи создания функции-шаблона (generic function) можно определить сущность алгоритма безотносительно к типу данных. После этого компилятор автоматически генерирует корректный код для того типа данных, для которого создается данная конкретная реализация функции на этапе компиляции. По существу, когда создается функция- шаблон, создается функция, которая может автоматически перегружать сама себя.
Функции-шаблоны создаются с использованием ключевого слова template (шаблон). Обычное значение слова «шаблон» достаточно полно отражает его использование в С++. Шаблон используется для создания каркаса функции, оставляя компилятору реализацию подробностей. Общая форма функции-шаблона имеет следующий вид:
template
{
// тело функции
}
Здесь птип является параметром-типом, «держателем места» (placeholder) для имени типа данных, которое используется функцией. Этот параметр-тип может быть использован в определении функции. Однако это только «держатель места», который будет автоматически заменен компилятором на фактический тип данных во время создания конкретной версии функции.
Ниже приведен короткий пример, в котором создается функция-шаблон, имеющая два параметра. Эта функция меняет между собой величины значений этих параметров. Поскольку общий процесс обмена значениями между двумя переменными не зависит от их типа, то он естественным способом может быть реализован с помощью функции-шаблона.
// пример шаблона функции
#include
// шаблон функции
template
{
X temp;
temp = a;
a = b;
b = temp;
}
int main()
{
int i=10, j = 20;
float x=10.1, у= 23.3;
char a="x", b="z";
cout << "Original i, j: " << i << " " << j << endl;
cout << "Original x, y: " << x << " " << у << endl;
cout << "Original a, b: " << a << " " << b << endl;
swap(i, j); // обмен целых
swap(x, у); // обмен вещественных значений
swap(a, b); // обмен символов
cout << "Swapped i, j: " << i << " " << j << endl;
cout << "Swapped x, y: " << x << " " << у << endl;
cout << "Swapped a, b: " << a << " " << b << endl;
return 0;
}
Рассмотрим эту программу более внимательно. Строка
Template
Указывает компилятору, что создается шаблон. Здесь X - шаблон типа, используемый в качестве параметра-типа. Далее следует объявление функции swap() с использованием типа данных X для тех параметров, которые будут обмениваться значениями. В функции main() функция swap() вызывается с передачей ей данных трех различных типов: целых чисел, чисел с плавающей запятой и символов. Поскольку функция swap() является функцией-шаблоном, то компилятор автоматически создаст три разные версии функции swap() - одну для работы с целыми числами, другую для работы с числами с плавающей запятой и, наконец, третью для работы с переменными символьного типа.