Программирование шаблонов C++ для идиотов, часть 1
Добавил(а) microsin
Многие программисты C++ избегают использования шаблонов (template) из-за их неоднозначной сущности. Вот какие аргументы приводят против использования шаблонов:
• Шаблоны трудно изучить, понять и адаптировать к применению. • Сообщения об ошибках компилятора, связанные с шаблонами, часто весьма непонятны, и очень большие по размеру. • Усилия, затраченные на изучение и применение шаблонов, не стоят выгод от их применения.
Вполне допускаю, что шаблоны несколько трудны для изучения, понимания и адаптации. Тем не менее преимущества, которые мы получаем от шаблонов, перевесят все отрицания. Это намного больше, чем стандартные функции и классы, которые могут быть обернуты вокруг шаблонов.
Шаблоны C++ и STL (Standard Template Library, стандартная библиотека шаблонов) технически являются родственниками. В этой статье шаблоны будут рассмотрены на базовом уровне. Вот основные темы:
• Синтаксис шаблонов • Шаблоны функции (Function Templates) Указатели (Pointers), ссылки (References) и массивы (Arrays) вместе с шаблонами Несколько разных типов и шаблоны функции (Function Templates) Function Template - Template Function Явная спецификация аргумента шаблона (Explicit Template Argument Specification) Аргументы по умолчанию с шаблонами функции • Шаблоны класса (Class Templates) Несколько разных типов с шаблонами класса Бестиповые аргументы шаблона (Non-type Template Arguments) Шаблон класса (Template Class) в качестве аргумента для шаблона класса (Class Template) Аргументы по умолчанию с шаблонами класса Методы класса в качестве шаблонов функции • Выводы
[Происхождение шаблонов]
Если кратко, то шаблоны - это расширение языка (в данном случае расширение языка C++ по отношению к языку C), позволяющее автоматизировать работу программиста. С шаблонами программист может уменьшить количество написанного кода, если нужно реализовать аналогичный функционал для различных исходных типов. Например, есть функция, и она должна работать для параметров разного типа. Конечно, можно написать несколько разных функций, или воспользоваться перегрузкой функций, но шаблоны предоставляют альтернативный путь. Т. е. для функции (или класса) имеется некий формализованный код (шаблон), в который передаются типы, и компилятор на основе этого сам строит рабочий код. Стоит задать себе вопрос - какова цель шаблонов, откуда в них появилась надобность?
Об этом хорошо рассказывается в лекции Евгения Линского "Шаблоны (templates). Вложенные классы. Пространство имен", которую можно найти на youtube. Цитата:
"Как люди жили, когда шаблонов не было? Стояла задача: нужно создавать отдельные классы, которые очень друг на друга похожи, но должны обрабатывать данные разных типов. К примеру, какие-то данные, которые находятся внутри класса. Предположим, у меня есть готовый класс, где в качестве данных используется тип int. И нужно получить новый класс, где int надо заменить на что-то другое. Как такая задача решается на классическом языке C?
Раньше это можно было реализовать с помощью директивы #define препроцессора. Давайте рассмотрим, как это можно сделать. Для автоматической генерации класса, обрабатывающего нужный тип, напишем макрос:
#define ARRAY (Type) \
class Array { \
Type* pData; \
... \
};
Примечание: по синтаксису этот макрос должен быть вытянут в одну строку. Как Вы знаете, слеш \ позволяет соединять строки, т. е. все, что идет после слеша, будет продолжать текущую строку.
Вообще, если учесть, что на обычном C не бывает классов, то пример не совсем удачный. Гораздо лучше было бы рассмотреть автогенерацию кода функций или структур на основе передаваемых в макрос типов.
Теперь на то место, где мне нужно будет подставить конкретный тип, я напишу Type. Это получился макрос с параметром, который позволяет определить класс для нужного типа. Например, в программе, где мне нужно это использовать, я могу написать:
ARRAY(int);
ARRAY(float);
Теперь везде, где мне нужно указать, что у меня будет лежать в массиве, на который указывает pData, параметр Type программист заменит на нужный тип. В результате чего препроцессор, когда обработает этот макрос, везде заменит вхождение параметра Type на подставленный тип, и появится нужный класс.
В таком простом примере скрыта проблема, которая решается дополнительными средствами препроцессора - конкатенацией. Если макрос ARRAY указан один раз, то код скомпилируется нормально, проблем не будет. Т. е. такое простое решение будет работать, если в одном модуле нужно задать массив не более чем одного типа. Но если макрос указан в одном файле 2 раза и больше, то возникнет конфликт имен - получится 2 одинаковых класса, с одинаковыми именами Array.
Конкатенация препроцессора # позволяет срастить 2 строки, и этим можно воспользоваться для модификации имени класса. Макрос будет выглядеть теперь так:
#define ARRAY (Type, Name) \
class Array#Name { \
Type* pData; \
... \
};
Тогда макрос для создания массивов можно вызывать так:
Это решение в стиле C, и оно вполне может использоваться, когда Вы программируете для микроконтроллеров, нет компилятора C++, и есть определенные ограничения на использование памяти.
Недостаток этого решения стандартный, им страдает любая программа, где задействованы макросы - код, который видите, это совсем не тот код, который видит компилятор. Поэтому легко могут возникнуть проблемы при поиске ошибок и отладке, потому что макросы для автоматической генерации кода имеют тенденцию становится довольно сложными для понимания. Например, когда Вы сделаете ошибку в макросе, то компилятор выдаст ошибку, и укажет номер ошибочной строки, но этот номер не будет соответствовать тому, как у Вас строки пронумерованы в файле исходного кода (потому что компилятору попадет код, который уже обработал препроцессор). Если Вы попытаетесь запустить код этого макроса в отладчике, то совсем не сможете отследить логику работы по исходному коду, потому что отладчик будет выполнять код, который сгенерировал препроцессор, а не тот, что Вы видите в окне среды разработки. Еще одна проблема: если где-то слово Type в макросе встретится или в составе переменной, или в имени функции, то препроцессор тоже подставит туда это слово, и будет ошибка.
Пояснение: препроцессор ничего не знает от типах в программе, он работает на уровне символов и слов (т. е. на уровне текстового файла), и заменяет одни символы на другие - как это определено правилами языка.
Шаблоны - это воплощение той же самой идеи, которая описана здесь, но не на уровне препроцессора, а на уровне компилятора. Т. е. когда компилятор компилирует код шаблона, он разбирается - где в коде типы, а где имена переменных, а потом производит подстановку в нужное места нужного кода.
Плюсы шаблонов по сравнению с автогенерацией кода на препроцессоре:
1. Не бывает проблем с ошибочными подстановками типа, как это могло бы произойти, когда параметр входит в имя функции или переменной. 2. Компилятор разбирает синтаксис кода в шаблоне, т. е. если Вы там допустили ошибку, то он правильно покажет в коде номер строки, где встретилась ошибка. 3. Нет проблем с работой отладчика по исходному коду."
[Синтаксис шаблонов]
Как Вы возможно уже знаете, шаблон широко использует в синтаксисе угловые скобки, соответствующие операторам меньше чем < и больше чем >. Для шаблонов эти угловые скобки используются вместе, вот так:
< содержимое >
Здесь "содержимое" может быть следующим:
1. Ключевыми словами class T или typename T (второй вариант полностью аналогичен первому). 2. Тип данных, который привязан к T. 3. Интегрированная спецификация. 4. Интегрированные константа / указатель / ссылка, которые привязаны к упомянутым спецификациям.
Пока все это звучит довольно непонятно, но на самом деле ничего сложного нет. Для пунктов 1 и 2 символ T обозначает всего лишь некоторый тип данных, который может быть любым базовым типом (int, double, и т. д.) или UDT (User Defined Type, т. е. тип, определенный пользователем).
Чтобы проще понять, что такое шаблоны и для каких целей они потребовались, давайте рассмотрим пример. Предположим, что Вы пишете функцию PrintTwice, которая печатает удвоенное значение числа:
Примечательно, что классовый тип ostream (это тип объекта cout) имеет множественные перегрузки оператора << для всех базовых типов данных. Таким образом, один и тот же код будет работать как для int, так и для double, и для перегрузки нашей функции PrintTwice не требуется никаких изменений - да, мы просто скопипастили её. Если бы мы использовали для вывода функцию printf, то две перегрузки функции PrintTwice выглядели бы следующим образом:
voidPrintTwice(int data)
{
printf("Удвоенное значение: %d", data *2 );
}
voidPrintTwice(double data)
{
printf("Удвоенное значение: %lf", data *2 );
}
Здесь основной смысл не в функции cout или о выводе текста в консоль, а в том, что код фактически остается тем же самым. Описанная ситуация - только одна из многих, в которых мы можем использовать замечательную фичу языка C++: шаблоны.
Шаблоны бывают двух типов:
• Шаблоны функции (Function Templates) • Шаблоны класса (Class Templates)
Шаблоны C++ являются моделью программирования, которые позволяют встраивать любой тип данных в код (код с шаблонами). Без шаблона нам бы понадобилось реплицировать один и тот же код снова и снова, для всех требуемых типов данных, которые использует код. И очевидно, что это потребует дополнительных усилий по поддержке кода. Так или иначе, с использованием шаблонов все очень упрощается, пример для нашей функции PrintTwice:
Здесь фактический тип TYPE будет выведен (автоматически определен) компилятором в зависимости от типа аргумента, который передан функции. Если PrintTwice была вызвана как PrintTwice(144); то это будет int, а если в функцию передано 3.14 то тогда TYPE будет выведен как тип double.
Вас может смутить - что это за TYPE, как компилятор определил, что это шаблон функции. Разве идентификатор TYPE был где-то определен ключевым словом typedef?
А вот и нет! Здесь мы используем вместо этого ключевое слово template, чтобы дать понять компилятору, что мы задали шаблон функции.
[Шаблоны функции (Function Templates)]
Вот шаблонная (templated) версия функции PrintTwice:
Первая строка кода template< class TYPE > (или template< typename TYPE >) говорит компилятору, что это шаблон функции (function-template). Действительное значение (т. е. какой будет тип) TYPE будет выведено компилятором зависимости от аргумента, который был передан в функцию. Здесь имя TYPE представляет так называемый параметр типа шаблона (template type parameter).
Например, если мы вызовем функцию так:
PrintTwice(124);
то TYPE будет заменено компилятором на int, и компилятор инициирует эту функцию-шаблон так:
Для Вашей программы это означает, что если Вы вызовете функцию PrintTwice с параметрами в одном случае типа int, а в другом случае типа double, то компилятором будет автоматически сгенерировано 2 экземпляра этой функции:
voidPrintTwice(int data) { ... }
voidPrintTwice(double data) { ... }
Да, размер кода удваивается. Однако эти две перегрузки функции реализуются не программистом вручную, а автоматически, компилятором. Это на самом деле выгодно, и Вам не нужно делать тупые операции по копипастингу одного и того же кода, или вручную поддерживать код для различных типов данных, или писать новые перегрузки для нового типа данных, который будет использоваться позже. Вы просто предоставляете шаблон для функции, и все остальное берет на себя компилятор.
Конечно же размер кода возрастает, потому что появляются новые (в нашем случае два) определения функции. Однако не всегда верно, что на двоичном уровне (ассемблер) размер кода будет расти пропорционально количеству перегрузок. Несмотря на то, что для N типов данных будет создано N инициаций функции (например перегруженной функции), оптимизация продвинутого компилятора / линкера несколько уменьшит размер кода, если инициированная функция будет та же самая, или у инициаций совпадают некоторые части кода.
Положительный момент для шаблонов: когда Вы вручную, без шаблонов, задаете N перегрузок функции (скажем, N=10), то эти N разных перегрузок будут скомпилированы, слинкованы и упакованы в двоичный исполняемый код. Но при использовании шаблонов в конечный исполняемый код попадут только требуемые инициации шаблона функции (только те, которые использовались в программе). С шаблонами в двоичном коде перегруженных копий может быть меньше чем N, и больше чем N - но только нужное количество копий, не больше и не меньше!
Также для реализаций без шаблонов, компилятор будет компилировать все эти N копий кода - поскольку они присутствуют в Вашем исходном коде. Когда Вы подсоединяете шаблон к обычной функции function, компилятор будет компилировать её только для необходимых типов данных. Это означает, что с шаблонами компиляция будет выполняться быстрее, если количество используемых типов данных меньше чем N.
Мне могут возразить, что компилятор/линкер все равно не передаст в выходной файл неиспользуемые перегрузки функции, когда шаблоны не используются. Однако, чтобы понять, какие перегрузки не нужны, компилятору все равно придется как минимум проверить синтаксис всех перегрузок. С шаблонами компиляция сразу будет происходить только для требуемых типов данных. Вы можете называть это "Компиляцией по запросу".
Теперь давайте напишем другой шаблон функции, которая возвращает удвоенное значение переданного в неё числа:
template<typename TYPE >
TYPE Twice(TYPE data)
{
return data *2;
}
Вы наверное заметили, что здесь используется ключевое слово typename вместо class. Нет, это не ошибка, и совсем не требуется использовать ключевое слово typename если функция что-то возвращает. Для программирования шаблонов эти два ключевых слова (class и typename) работают абсолютно одинаково. Есть просто историческая причина, почему эти два ключевые слова имеют одинаковое значение (ненавижу историю, не хватало еще и ей все запутывать...).
Однако есть экземпляры, где Вы можете использовать только более новое ключевое слово - typename (когда частный тип определен в другом типе, и это зависит от некоторого параметра шаблона - отложим обсуждение этого для другого случая).
• В третьей строке примера кода использования шаблонной функции она была вызвана дважды - результат возврата одного вызова используется в качестве аргумента для второго вызова. Следовательно, оба вызова работают с типом int (и аргумент, и тип возвращаемого значения имеют один и тот же тип TYPE). • Если шаблонная функция была инициирована для отдельного типа данных, то для повторного вызова функции с тем же типом компилятор будет повторно использовать тот же самый, уже инициированный экземпляр функции. Причем не имеет значения, где происходили эти вызовы - или в одной функции, или в пределах одного модуля, или даже в другом модуле исходного кода - всегда для тех же самых используемых типов будет использоваться один и тот же инициированный экземпляр функции.
Напишем шаблонную функцию, которая возвращает сумму двух чисел:
template<classT>
T Add(T n1, T n2)
{
return n1 + n2;
}
Во-первых, здесь я заменил имя параметра типа: раньше оно было TYPE, а теперь просто символ T. В программировании шаблонов общей практикой является использование для имени типа букву T, но это Ваш личный выбор. Лучше использовать такое имя, которое будет лучше отражать назначение типа параметра, и/или улучшит читаемость кода. Этот символ может быть любым именем, которое соответствует правилам именования переменных языка C++.
Во-вторых, я дважды использовал параметр шаблона T - для обоих переменных аргумента функции (n1 и n2).
Давайте слегка изменим нашу функцию Add, чтобы в ней результат сложения сохранялся сначала в переменную, и затем эта переменная возвращалась в качестве результата функции.
template<classT>
T Add(T n1, T n2)
{
T result;
result = n1 + n2;
return result;
}
Вы можете спросить (и Вы должны спросить): "Как компилятор определит тип результата, когда он попытается скомпилировать (сделать парсинг) функции Add?"
ОК, когда компилятор будет просматривать тело функции (Add), то он не увидит, корректен ли T, или нет (параметр типа шаблона). Он просто проверит базовый синтаксис (типа наличие точек с запятой, правильность использования ключевых слов, соответствие открытых и закрытых скобок и т. д.), сообщит об ошибках, которые встретились в результате этих базовых проверок. И снова, уже от реализации компилятора зависит (т. е. разные компиляторы могут вести себя по-разному), как он обработает код шаблона - но не будет сообщено ни о каких ошибок из-за параметров типа шаблона.
Однако для полноты я бы повторил, что компилятор не проверит если (в настоящий момент это относится только к функции Add):
• T имеет конструктор по умолчанию (так что будет допустимым T result;) • T поддерживает использование оператора + (так что n1+n2 допустимо) • T доступен для конструктора копирования/перемещения (так что оператор возврата return сработает)
По существу компилятор должен был бы скомпилировать код шаблонов за 2 фазы: сначала проверка базового синтаксиса; и потом инициация шаблона функции - где компилятор должен уже выполнить реальную компиляцию кода в соответствии с типом данных шаблона.
[Указатели (Pointers), ссылки (References) и массивы (Arrays) вместе с шаблонами]
Сначала посмотрим на еще один простой пример:
template<classT>
double GetAverage(T tArray[], int nElements)
{
T tSum = T(); // tSum = 0for (int nIndex =0; nIndex < nElements; ++nIndex)
{
tSum += tArray[nIndex];
}
// Независимо, от того, какой тип T, он будет преобразован в тип double:returndouble(tSum) / nElements;
}
Для первого вызова GetAverage, когда передается массив целых чисел IntArray, компилятор инициирует эту функцию так:
doubleGetAverage(int tArray[], int nElements);
Подобным образом будет сделано и для float. Возвращаемый тип будет сохранен как double, поскольку для среднего значения чисел массива логически подходит тип данных double. Обратите внимание, что это верно только для этого примера - действительный тип данных, который поступил под T, может быть классом, который нельзя преобразовать в double.
Вы должны заметить, что у функции могут быть как шаблонные параметры, так и обычные. Не требуется, чтобы все аргументы шаблонной функции поступали от шаблонных типов. В этом примере не шаблонный аргумент это int nElements.
Четко отметьте и поймите, что параметр шаблонного типа это просто T, и не T* или не T[] - компиляторы достаточно умны, чтобы вывести тип int из int[] (или int*). В выше приведенном примере в качестве аргумента использовался T tArray[] как аргумент для шаблонной функции, и действительный тип данных T был разумно определен.
Вы часто столкнетесь с необходимостью использовать инициализацию наподобие:
T tSum = T();
Первое, что следует заметить - это вовсе не специфика кода шаблона, этот синтаксис пришел из языка C++. По существу это означает: вызвать конструктор по умолчанию для этого типа данных. Для int это было бы:
int tSum =int();
Данное действие проинициирует переменную значением 0. Подобным образом для float переменная была бы установлена в значение 0.0f. Хотя это не относится к данному примеру, если бы вместе с T поступал тип класса, определенный пользователем, то был бы вызван конструктор по умолчанию для этого класса (если он может быть вызван, иначе произойдет соответствующая ошибка). Как Вы можете понять, T может быть любым типом данных, мы не можем инициализировать tSum просто целочисленным нулем (0). В реальном случае это может быть и строковый класс, который инициализируется пустой строкой ("").
Поскольку шаблонный тип T может быть любым, у него должен быть доступен оператор +=. Как мы знаем, он доступен для всех базовых типов данных (int, float, char и т. д.). Если у действительного типа данных (для T) нет доступного оператора += (или нет никакой возможности для этого), компилятор вызовет ошибку и сообщит о том, что у этого типа нет этого оператора, или нет любого возможного преобразования.
Подобным образом тип T должен быть способен преобразовать самого себя в double (см. оператор возврата return). Эти основные моменты будут рассмотрены позже. Для лучшего понимания я переупорядочиваю требуемую поддержку со стороны типа T (это применимо сейчас только для этого примера шаблонной функции GetAverage):
• Должен быть доступен конструктор по умолчанию. • Должен быть вызываемый оператор +=. • Тип должен быть способен преобразовать самого себя в double (или эквивалентный тип).
Для прототипа шаблона функции GetAverage Вы можете использовать T* вместо T[], и это будет означать то же самое:
template<classT>
GetAverage(T* tArray, int nElements){}
Вызывающий код мог бы передать массив (выделенный в стеке или куче), или адрес переменной типа T. Но Вы должны знать, что эти правила поступают от в соответствии с правилами языка C++, и это не относится специально к программированию шаблона.
Давайте теперь попросим, чтобы в программировании шаблона был задействован агент 'ссылка' (reference). Довольно очевидно, что Вы просто используете T& в качестве аргумента шаблонной функции для нижележащего типа T:
Эта функция вычислит двойное значение от аргумента, и поместит это значение на место самого аргумента. Вы вызвали бы эту функцию просто:
int x =40;
TwiceIt(x); // В результате в переменной x будет 80
Обратите внимание, что я использовал оператор *= для получения двойного значения аргумента tData. Вы можете с таким же успехом использовать оператор +. Для базовых типов данных доступны оба оператора. Для классового типа совсем необязательно будут доступны оба оператора, и Вы могли бы запросить класс реализовать требуемый оператор.
ИМХО логично попросить, чтобы оператор + был бы определен классом. Причина проста - действие T+T больше подходит для большинства UDT (User Defined Type, определенный пользователем тип), чем оператор *=. Спросите себя: что могло бы значить для реализаций класса String или Date, или для реализации следующего оператора:
voidoperator*= (int); // возвращаемый тип void (только для упрощения)
Сейчас Вы четко понимаете, что тип параметра шаблона T может быть выведен из T&, T* или T[]. Поэтому также возможно и очень разумно добавить атрибут const к параметру, который поступает к шаблону функции, и этот параметр не должен был быть изменен шаблонной функцией. Успокойтесь, все просто:
Заметьте, что я модифицировал параметр шаблона TYPE, изменив его на TYPE&, и также добавил к нему const. Кое-кто или многие из читателей поняли важность этого изменения. Для тех, кто не понял:
• Тип TYPE может быть большим по размеру, и потребовать больше места в стеке (стек вызовов функций, call-stack). Это касается типа double, который требует 8 байт(1), некоторых структур или классов, которые потребовали бы больше байтов для сохранения в стеке. По существу это означает следующее - должен создаться новый объект, вызван конструктор копирования, данные объекта помещены в стек вызовов, и впоследствии был бы вызван деструктор объекта по завершении функции.
Добавление ссылки (reference &) позволяет избежать всего этого - передается ссылка на уже имеющийся объект.
• Функция не изменила бы переданный аргумент, потому что к нему был добавлен атрибут const. Это гарантирует, что код, вызывающий эту функцию (здесь PrintTwice), не изменит значение параметра. Это также гарантирует выдачу ошибки компилятором, если функция сама по себе попытается изменить содержимое аргумента (константы).
Примечание (1): на 32-разрядных платформах аргументы функции требовали бы 4 байта минимум, или количество байт, нацело делимое на 4. Это означает, что char или short потребуют 4 байта в стеке вызовов. К примеру, 11-байтный объект потребует в стеке пространства из 12 байт.
Подобным образом для 64-битных платформ понадобится минимум 8 байт. 11-байтный объект займет в стеке 16 байт. Аргумент типа double потребует 8 байт.
Все указатели и ссылки (pointers и references) займут соответственно 4 и 8 байт на 32-bit и 64-bit платформах, так что передача double или double& для 64-разрядной платформы будет означать одно и то же.
Похожим образом мы должны были бы изменить другие шаблоны функций:
template<classTYPE>
TYPE Twice(const TYPE& data) // Нет никакого изменения для типа возврата
{
return data *2;
}
template<classT>
T Add(const T& n1, const T& n2) // Нет изменения типа возврата
{
return n1 + n2;
}
template<classT>
GetAverage(const T tArray[], int nElements)// GetAverage(const T* tArray, int nElements)
{}
Обратите внимание, что нельзя иметь добавленную ссылку (reference) и добавленный атрибут const к возвращаемому типу, если мы не намереваемся возвратить ссылку (или указатель) на оригинальный объект, который был передан в шаблон функции. Следующий код иллюстрирует это:
// Установить максимальное значение в ноль (0)
GetMax(x,y) =0;
Имейте в виду, что это сделано просто для иллюстрации, и Вы редко увидите или напишете подобный код. Однако возможно, что Вы все-таки столкнетесь с таким кодом, если возвращаемый объект это ссылка на какой-то UDT. В этом случае за вызовом функции следовал бы оператор доступа 'точка' (.) или 'стрелочка' (->). Так или иначе этот шаблон функции возвращает ссылку на объект. Это определенно требует, чтобы оператор > был определен типом T.
Вы должны были бы заметить, что я не добавил const ни к одному из переданных параметров. Это нужно сделать, так как функция возвращает ссылку на тип T, не константу. Если бы это было возможно, то так:
T& GetMax(const T& t1, const T& t2)
В операторах возврата компилятор жаловался бы, что t1 или t2 нельзя преобразовать в не константу. Если мы добавим const также и к возвращаемому типу ( const T& GetMax(...) ), то следующая строка не скомпилируется:
GetMax(x,y) =0;
Объект const не может быть модифицирован! Вы можете принудительно сделать приведение типа const/non-const, либо в самой функции, либо в месте вызова. Но это уже другой вопрос, плохой дизайн и не рекомендуемый метод работы.
[Несколько разных типов (Multiple Types) с шаблонами функции]
Пока я рассмотрел только шаблон с одним типом параметра. С шаблонами Вы можете иметь больше чем один тип параметра шаблона:
template<classT1, classT2, ... >
Здесь T1 и T2 имена типов для шаблона функции. Вы можете использовать любые другие имена, отличающиеся от T1, T2 и т. д. Обратите также внимание, что многоточие здесь '...' означает, что стандарт шаблонов предусматривает возможность принять любое количество аргументов во время выполнения. Это просто показывает, что при определении шаблона (во время компиляции) можно задать любое нужное (определенное!) количество аргументов.
Примечание: в соответствии со стандартом C++11 шаблоны позволяют использовать переменное количество аргументов, но это отдельная тема, которая здесь не рассматривается.
Давайте рассмотрим простой пример, получающий два параметра шаблона:
PrintNumbers(10, 100); // int, int
PrintNumbers(14, 14.5); // int, double
PrintNumbers(59.66, 150); // double, int
Здесь каждый вызовет генерацию компилятором отдельной инициации шаблона в зависимости от комбинации используемых типов аргументов при вызове. Поэтому компилятор автоматически создаст 3 перегрузки функции PrintNumbers:
// Для упрощения понимания были удалены const и ссылки
voidPrintNumbers(int t1Data, int t2Data);
voidPrintNumbers(int t1Data, double t2Data);
voidPrintNumbers(double t1Data, int t2Data);
Реализация второй и третьей инициаций не та же самая, потому что T1 и T2 вовлекут разные типы данных (int, double и double, int). Компилятор не выполнит какое-либо автоматическое преобразование типа, как это могло быть сделано для обычного вызова функции - например, обычная функция получает int, но может быть передан short, или наоборот. Но с шаблонами если Вы передадите short, то это будет абсолютно short, а не (обновленный к типу) int. Так что если мы передадим варианты (short, int), (short, short), (long, int), то в результате получим 3 разные инициации для функции PrintNumbers.
Точно так же у шаблонов функций может быть 3 или большее количество разных типов параметров, и каждый из них отобразится на типы аргумента, указанные при вызове функции. Например, следующий тип шаблона функции легален:
Здесь T1 задает тип элементов массива, который был бы передан кодом, вызывающим функцию. Если массив (или указатель) не был передан, то компилятор выдаст соответствующую ошибку. Тип T2 используется также и как тип возвращаемого значения, а также как тип второго аргумента, который передается по значению. Тип T3 передается по ссылке (ссылке на не константу). Этот пример функции выбран случайно - просто для демонстрации допустимой с точки зрения стандарта реализации шаблона функции.
В настоящее время мы разобрали применение в шаблоне нескольких типов. Сейчас я снова вернусь к шаблонам с одним параметрам, и Вы быстро поймете, почему.
Предположим, есть функция (обычная, не шаблонная), которая принимает аргумент типа int:
• Вызов 1 абсолютно допустим, потому что функция по определению принимает аргумент int, и мы передали 120. • Вызов 2 допустим, потому что мы передали тип char, который компилятором будет преобразован в int. • Вызов 3 потребовал бы преобразования параметра с потерей - компилятор преобразует тип double в тип int, и таким образом в функцию на самом деле будет передано 55 вместо 55.64. Да, это вызовет вывод соответствующего предупреждения компилятором.
Одно из решений - модифицировать функцию, чтобы она принимала double, тогда в функцию могут быть переданы без потерь все 3 типа. Однако это не поддерживало бы все типы, которые возможно не могли бы быть преобразованы в double или подходить под него. Поэтому Вы можете написать набор перегруженных функций, принимающих соответствующие типы. Но теперь, вооруженные знаниями шаблонов, Вы можете вместо этого написать шаблон функции:
template<classType>
void Show(Type tData) {}
Конечно подразумевается, что все имеющиеся перегрузки функции Show делают одно и то же.
Хорошо, Вы знаете этот метод. Но почему мне понадобилось снова рассматривать шаблон с одной переменной? ОК, что будет, если Вы хотите передать int шаблону функции Show, но хотите, чтобы компилятор инициировал функцию, как если бы было передано double?
// Этот вызов сгенерирует инициацию 'Show(int)'
Show ( 1234 );
// Но Вы хотели бы инициировать в этом месте 'Show(double)'...
Такое требование может показаться нелогичным, но предположим, что оно есть. По некоторым причинам нужно запросить именно такую инициацию функции, и Вы скоро поймете, почему.
Так или иначе эта "абсурдная" вещь делается следующим образом:
Show<double> ( 1234 );
Этот вызов приведет к инициации шаблона функции в таком виде:
voidShow(double);
Здесь показан специальный синтаксис шаблонов (Show< >()), с которым Вы можете запросить компилятор сделать инициацию функции Show явно указанного типа, и запросить у компилятора не выводить самому тип аргумента у функции.
[Function Template и Template Function]
Важный момент! Настала пора разобраться, в чем разница между шаблонной функцией (function template, шаблон функции) и функцией шаблона (template function).
Шаблон функции (function template) это тело функции, заключенное в скобки вокруг ключевого слова template, которая на самом деле не действительная функция, и она не полностью, как есть, компилируется компилятором, и не учитывается линкером. Для реальной инициации этой функции нужен как минимум один её вызов, где будут задействован какой-то частный набор типов данных, нужный для определенной инициации этой функции, и который будет учтен компилятором и линкером. Поэтому код шаблона функции будет инициирован как Show(int) или Show(double).
А что с функцией шаблона (template function)? Проще говоря, "экземпляр шаблона функции", который был сгенерирован при её вызове, или заставленный инициироваться для определенного типа данных. Экземпляр функции шаблона действительно является допустимой функцией.
Код шаблонной функции (так называемый шаблон функции) не является нормальной функцией, эта функция находится под зонтиком декорации системы имен компилятора и линкера. Это означает, что код шаблонной функции:
template<classT>
void Show(T data)
{ }
для аргумента шаблона double не будет таким:
voidShow(double data){}
а на самом деле будет таким:
void Show<double>(double x){}
Для long я не рассматривал как все получится, но Вы уже и так разобрались, что к чему. Используйте Ваш компилятор и отладчик, чтобы найди актуальную инициацию шаблона функции, и посмотрите на полный прототип этой функции в стеке вызовов или в сгенерированном коде.
И следовательно, Вы теперь знаете взаимосвязь между этими двумя понятиями:
Show<double>(1234);
...
void Show<double>(double data); // Обратите внимание, в этом случае data=1234.00!
И у нас есть 3 вызова функции, которые приведут к 3 разным инициациям этого шаблона функции:
PrintNumbers(10, 100); // int, int
PrintNumbers(14, 14.5); // int, double
PrintNumbers(59.66, 150); // double, int
И что если нам нужно иметь только одну инициацию - чтобы оба аргумента воспринимались как double? Да, мы будем передавать int, и позволим их преобразовать в double. Теперь понятно, что с описанным шаблоном этого можно добиться так:
PrintNumbers<double, double>(10, 100); // int, int
PrintNumbers<double, double>(14, 14.5); // int, double
PrintNumbers<double, double>(59.66, 150); // double, int
Все эти 3 вызова дадут только одну инициацию функции шаблона:
Концепция, при которой таким способом передается тип параметра шаблона со стороны вызывающего кода, известна как Explicit Template Argument Specification (явное указание аргумента шаблона).
• Причина 1: Вы хотите передавать только определенный тип, и не позволить компилятору умничать в принятии решения при выборе типа для одного или большего количества аргументов шаблона (на основе фактически переданных в функцию параметров).
Например, вот один шаблон функции max, принимающей 2 аргумента (аргументы могут быть только с однотипные):
template<classT>
T max(T t1, T t2)
{
if (t1 > t2)
return t1;
return t2;
}
И если вы попытаетесь вызвать эту функцию так:
max(120, 14.55);
то это приведет к ошибке компилятора, напоминающей о неоднозначности с типом параметра шаблона T. Вы требуете от компилятора выбрать один тип из двух типов! Одно из решений - поменять шаблон max так, чтобы она получала два типа параметров - но Вы не автор этого шаблона функции.
Тогда Вы просто используете явное указание типа аргумента:
max<double>(120, 14.55); // это инициирует функцию шаблона max< double >(double,double);
Обратите внимание и поймите, что я явно задал и передал только первый параметр шаблона, второй тип был выбран на основе типа второго аргумента в вызове функции.
• Причина 2: когда шаблон функции получает тип аргумента, но у него нет аргументов для функции.
Простой пример:
template<classT>
void PrintSize()
{
cout <<"Размер этого типа:"<<sizeof(T);
}
Вы не можете просто вызвать эту функцию вот таким способом:
PrintSize();
Поскольку этот шаблон функции требует указания типа аргумента шаблона, он не может быть автоматически вычислен компилятором. Корректный вызов будет таким:
PrintSize<float>();
Это приведет к инициации PrintSize с аргументом шаблона типа float.
• Причина 3: когда функция шаблона имеет возвращаемый тип, который не может быть выведен на основании аргументов, или когда у шаблона функции нет аргументов.
Пример:
template<classT>
T SumOfNumbers(int a, int b)
{
T t = T(); // вызов конструктора по умолчанию для T
t = T(a)+b;
return t;
}
Функция получает в качестве параметров два числа типа int и складывает их. Суммируя числа в виде int, как и положено, этот шаблон функции дает возможность выдать сумму (используя оператор +) в любом типе, который затребовал вызывающий код. Например, чтобы получить результат в виде double, Вы должны вызвать функцию так:
Последние 2 примера являются упрощенными для полноты - просто чтобы дать Вам подсказку, где может пригодиться явное указание типа аргумента шаблона (Explicit Template Argument Specification). Более приземленные сценарии, где эта явность будет необходима, будут рассмотрены в следующей части.
[Аргументы по умолчанию и шаблоны функции]
Для читателей, кто знаком со спецификацией шаблонов в контексте типа по умолчанию шаблона - здесь не имеются в виду типы аргумента по умолчанию для шаблона. Так или иначе типы по умолчанию не разрешены для шаблонов функций. Читателям, кто не знает об этом, можно не беспокоиться - этот параграф не о спецификации шаблонов для типа шаблона по умолчанию.
Как Вы знаете, функция C++ может иметь аргументы по умолчанию. Умолчание работает только справа налево. Это означает, что если требуется n-ный аргумент по умолчанию, то обязательно должен быть по умолчанию и аргумент (n+1), и так далее до последнего аргумента в функции (все аргументы от n+1 до последнего должны быть по умолчанию, или эти аргументы должны отсутствовать).
Простой пример, который поясняет это:
template<classT>
void PrintNumbers(T array[], int array_size, T filter = T())
{
for(int nIndex =0; nIndex < array_size; ++nIndex)
{
if ( array[nIndex] != filter) // Печать, если не отфильтровано
cout << array[nIndex];
}
}
Этот шаблон функции печатает, как Вы можете догадаться, все числа за исключением тех, которые были отфильтрованы третьим аргументом filter. Последний, необязательный аргумент функции сделан по умолчанию на значение по умолчанию для типа T. Это для всех базовых типов означает 0. Таким образом, если при вызове функции мы пропустим последний аргумент:
int Array[10] = {1,2,0,3,4,2,5,6,0,7};
PrintNumbers(Array, 10);
то он будет инициирован как:
voidPrintNumbers(int array[], int array_size, int filter =int())
{}
Аргумент filter будет вычислен как int filter = 0.
Само собой, когда Вы делаете вызов:
PrintNumbers(Array, 10, 2);
то третий элемент получит значение 2, не значение по умолчанию 0. Вы должны четко усвоить:
• Тип T должен иметь доступный конструктор по умолчанию. И конечно, все операторы, которые нужны для вычислений в теле функции для типа T. • Аргумент по умолчанию должен быть выводим от других, не умолчательных типов аргументов, которые получает шаблон. В примере PrintNumbers на основе типа массива упростится вывод типа для filter. Если же нет, то Вы должны использовать явное указание типа аргумента (explicit template argument specification), чтобы задать тип аргумента по умолчанию.
Аргумент по умолчанию не обязательно может быть значением по умолчанию для типа T. Это означает, что аргумент по умолчанию не всегда может нуждаться в зависимости от конструктора по умолчанию для типа T:
template<classT>
void PrintNumbers(T array[], int array_size, T filter = T(60))
Здесь аргумент функции по умолчанию не использует значение по умолчанию для типа T. Вместо этого используется значение 60. Это требует для типа T наличие copy-конструктора, который принимает int (для числа 60).
[Шаблоны класса]
Наверняка чаще Вам придется проектировать и использовать шаблоны класса (class templates), а не шаблоны функции (function templates). В общем Вы используете шаблон класса чтобы определить абстрактный тип, поведение которого универсальное, и допускающее повторное и адаптируемое использование. Начну с простых примеров, которые можно проще понять.
Давайте представим простой класс, который устанавливает, получает и печатает сохраненное значение:
В конструкторе данные Data инициализируются в 0, есть методы установки (SetData) и выборки (GetData) данных, и метод вывода текущего значения данных на печать (PrintData). Использование этого класса также очень простое:
Тут ничего нет нового для Вас, конечно! Но когда Вам нужна подобная абстракция для другого типа данных, Вам нужно дублировать код всего класса (или как минимум для требуемых методов). Это означает наличие дополнительных проблем по поддержке кода, увеличению его объема и в исходниках, и в виде исполняемого кода.
Конечно Вы уже догадались, что для этой цели я перейду к шаблонам C++! Шаблонная версия для того же класса в виде шаблона класса будет следующая:
Декларация шаблона класса начинается с такого же синтаксиса, который мы видели в шаблоне функции:
template<classT>
classItem
Обратите внимание что ключевое слово class используется дважды - сначала для указания спецификации типа шаблона (T), и второй раз чтобы указать, что это декларация класса C++.
Чтобы полностью превратить Item в шаблон класса, я заменил все вхождения int на T. Я также в конструкторе использовал синтаксис T() для вызова конструктора по умолчанию для T, вместо жестко закодированного присвоения 0. Если Вы полностью прочитали секцию шаблонов функции, то Вы знаете, почему так.
В отличие от инициации функции шаблона, где аргументы вызова функции сами по себе помогают компилятору вывести типы аргумента шаблона, с шаблонами класса Вы должны явно передать тип шаблона (в угловых скобках).
Только что показанный пример приведет к инициации шаблона класса Item как Item< int >. Когда Вы создаете другой объект с отличающимся типом, то используйте шаблон класса Item например так:
Item<float> item2;
float n = item2.GetData();
Это приведет к инициации Item< float >. Важно знать, что нет абсолютно никакой взаимосвязи между двумя инициациями шаблона класса - между Item< int > и Item< float >. Для компилятора и линкера это будут две абсолютно разные сущности - или, можно сказать, разные классы.
Первая инициация с типом int создаст следующие методы:
• Конструктор Item< int >::Item() • Методы SetData и PrintData для типа int
Подобным образом вторая инициация с типом float создаст:
• Конструктор Item< float >::Item() • Метод GetData для типа float
Как Вы знаете, Item< int > и Item< float > два разных класса / типа, так что такой код работать не будет:
item1 = item2; // ERROR : Item< float > to Item< int >// (ошибка присвоения друг другу разных типов)
Поскольку оба типа разные, то компилятор не сможет вызвать оператор присваивания по умолчанию. Если бы у item1 и item2 были одинаковые типы (скажем, у обоих тип Item< int >), то компилятор успешно обработает оператор присваивания. Несмотря на то, что компилятор может делать преобразование между int и float (с выдачей предупреждения), но невозможно делать преобразования для разных UDT, даже если нижележащие поля данных одинаковые - просто такие правила языка C++.
В этой точке будет инициирован только следующий набор методов:
• Item< int >::Item() - конструктор • void Item< int >::SetData(int) метод • void Item< int >::PrintData() const метод • Item< float >::Item() - конструктор • float Item< float >::GetData() const метод
Следующие метода не пройдут вторую фазу компиляции:
• int Item< int >::GetData() const • void Item< float >::SetData(float) • void Item< float >::PrintData() const
Так, что это за "вторая фаза компиляции"? Ранее я упоминал о том, что код шаблона будет компилироваться с базовой проверкой синтаксиса, независимо от того, был ли он вызван / инициирован или нет. Это и есть так называемая первая фаза компиляции.
Когда Вы действительно вызываете метод, или как-нибудь вызываете срабатывание вызова функции / метода для частного типа (или типов) - только тогда выполняется специальная обработка кода метода, называемая второй фазой компиляции. Только после второй фазы компиляции код компилируется полностью, с использованием того типа, который был инициирован.
Как Вы узнаете, прошла ли функция первую фазу или вторую фазу компиляции? Давайте сделаем нечто странное:
T GetData() const
{
for())
return Data;
}
Здесь есть лишняя скобка в конце цикла for - это некорректно. Когда Вы попытаетесь скомпилировать этот код, то получите сообщение об ошибке - независимо от того, вызывался этот код где-либо или нет. Я это проверил как на компиляторе Visual C++, так и на GCC, так везде. Это означает выполнение первой фазы компиляции.
Сделаем небольшие изменения:
T GetData() const
{
T temp = Data[0]; // доступ по индексу ?return Data;
}
Теперь скомпилируйте код без вызова GetData для любого типа. Компилятор не выдаст ошибку, потому что в этом месте функция не получит обработки на второй фазе компиляции. Но как только Вы сделаете вызов:
Item<double> item3;
item2.GetData();
то получите ошибку от компилятора "Data is not an array or pointer" (Data не является массивом или указателем), из-за оператора []. Оказывается, что только выбранные функции получили бы специальную привилегию на вторую фазу компиляции. И эта вторая фаза компиляции будет выполнена отдельно для всех уникальных типов, которые Вы инициировали для класса / функции.
Одна интересная вещь, Вы можете сделать так:
T GetData() const
{
return Data %10;
}
Это успешно скомпилируется для Item< int >, но для Item< float > будет ошибка:
item1.GetData(); // item1 как Item< int >// ERROR
item2.GetData(); // item2 как Item< float >
Причина в том, что оператор % неприменим для типа float.
[Шаблоны класса с несколькими типами]
Наш первый шаблон класса имел только один тип в шаблоне. Давайте создадим класс, у которого 2 аргумента типа для шаблона. Программисты, использующие STL, найдут эквивалент этого примера в шаблоне класса std::pair.
Предположим, что у Вас есть структура Point,
struct Point
{
int x;
int y;
};
в которой 2 поля данных. Потом у Вас может также быть другая структура Money:
struct Money
{
int Dollars;
int Cents;
};
Обе эти структуры имеют практически одинаковые типы полей данных. Вместо того, чтобы писать новые структуры, возможно было бы лучше держать структуру в одном месте, которое также упростит:
• У конструктора есть один или два аргумента указанного типа, и copy-конструктор. • Методы для сравнения двух объектов одинакового типа. • Перестановка между двумя типами • И другие операции.
Вы можете сказать, что для этой цели есть модель наследования (inheritance), где Вы определили бы все требуемые методы в базовом классе и от него вывели бы нужный дочерний класс, подстроенный под нужные цели. Это подойдет? А что делать с выбором типов данных? В качестве типа может быть int, string, float или некоторый класс. Если коротко, то наследование только усложнит дизайн, и не позволит применить фичу плагинов, упрощаемую шаблонами C++.
Здесь мы будем использовать шаблоны класса! Просто определите шаблон класса для 2 типов, имеющих все требуемые методы.
template<classType1, classType2>
struct Pair
{
// Эти поля будут в области public, потому что нам нужно, чтобы// клиенты использовали эти поля напрямую.
Type1 first;
Type2 second;
};
Теперь мы можем использовать шаблон класса Pair, чтобы получить любой тип, в котором есть 2 поля. Например:
// В качестве структуры Point
Pair<int, int> point1;
// Логически то же самое для полей X и Y
point1.first =10;
point1.second =20;
В этом случае типы первого и второго полей int и int соответственно. Это потому, что мы инициировали Pair этими типами.
то первое поле будет типа int, и второе поле будет типа double. Понятно, что первый и второй поля являются просто полями данных, это не функции, так что нет никаких затрат ресурсов во время выполнения вызова исходной функции.
Примечание: в этой части статьи все определения делаются только в пределах тела декларации класса. В следующей части я покажу, как реализовать методы в отдельном файле реализации и связанные с этим проблемы. Таким образом, все определения методов должны быть приняты только в ClassName{...};.
Следующий конструктор по умолчанию инициировал бы обоих членов их значениями по умолчанию согласно типам данных Type1 и Type2:
Pair() : first(Type1()), second(Type2())
{}
Следующий параметризированный конструктор получает Type1 и Type2 для инициализации значений для полей first и second:
Обратите внимание на то, что нужно обязательно указывать аргументы типа в шаблоне Pair< > для аргумента в этом copy-конструкторе. Следующая спецификация не была бы целесообразна, поскольку у Pair не шаблонный тип:
Pair(const Pair& OtherPair) // ERROR: Pair requires template-types// (ошибка: для Pair требуется типы шаблона)
Вот примеры использования параметризированного конструктора и copy-конструктора:
Важно отметить, что если Вы поменяете любые параметры типа шаблона или объектов point2 или point1, Вы не сможете использовать copy-конструктор для объекта point1. Здесь возникнет ошибка:
Pair<int, float> point2(point1); // ERROR: Different types, no conversion possible.// (ошибка: разные типы, преобразование невозможно)
Хотя компилятор может сделать конверсию при переводе float в int (с выдачей предупреждения), однако нет возможности конверсии от Pair< int, float > к Pair< int, int >. Конструктор копирования не может взять для источника копирования объект другого типа. Есть решение для этой проблемы, которое будет рассмотрена далее.
Точно таким же образом Вы можете реализовать операторы сравнения, чтобы сравнить два объекта одинакового типа Pair. Вот реализация оператора эквивалентности (equal-to):
booloperator== (const Pair< Type1, Type2 >& Other) const
{
return first == Other.first &&
second == Other.second;
}
Обратите внимание, что я использовал атрибут const для самого метода. Как и при вызове copy-конструктора, Вы должны передать абсолютно те же самые типы для этого оператора сравнения - компилятор не будет пытаться преобразовать разные типы Pair. Например:
if (point1 == point2) // оба сравниваемых объекта должны быть одного типа.
...
Для полного понимания концепции, рассмотренной здесь, пожалуйста реализуйте следующие методы самостоятельно:
• Все остальные 5 операторов отношения • Оператор присваивания • Метод перестановки (Swap) • Модифицируйте оба конструктора (за исключением copy-конструктора), и скомбинируете его в один, который взял бы опа параметра по умолчанию. Это означает, что нужно реализовать только один конструктор, который может получать 0, 1 или 2 аргумента.
Для класса Pair даны примеры для 2 типов, и он может быть использован вместо того, чтобы определять множество структур, у которых просто 2 типа данных. Недостаток тут только в том, что надо помнить, что first и second параметры могут что-то означать (X или Y?). Но если Вы хорошо определите инициацию шаблона, то всегда будете это знать, и правильно использовать поля first и second.
Не обращая внимания на этот недостаток, Вы достигли бы всех возможностей в инициированном типе: конструкторы, copy-конструктор, операторы сравнения, метод swap и т. д. И Вы получили бы все это без переписывания требуемого кода для разных структур с двумя полями, которые могли бы потребоваться. Кроме того, как Вы уже знаете, будет скомпилирован и слинкован только требуемый набор методов. Исправление ошибки в шаблоне класса автоматически отразится на все инициации. Да, небольшая модификация класса может также повысить количество выдаваемых ошибок для других типов, если модификации окажутся несовместимыми с текущим использованием.
Аналогично у Вас может быть шаблон класса, который позволяет 3 (или большее количество) полей данных. Пожалуйста, попробуйте самостоятельно реализовать класс tuple с тремя полями (first, second, third):
template<classT1, classT2, classT3>
classtuple
[Аргументы шаблона без типа]
Хорошо, мы увидели, что как и с шаблоны функции, шаблоны класса может получать аргументы нескольких типов. Однако шаблоны класса позволяют также иметь несколько аргументов шаблона без типа (non-type template arguments). В этой части я рассмотрю только один non-type аргумент: целое число.
Да, шаблон класса может получать целое число (integer) как аргумент шаблона. Первый пример:
template<classT, int SIZE >
classArray{};
В этой декларации класса шаблона int SIZE является non-type аргументом, который целое число.
• Только целочисленные типы данных могут быть non-type integer аргументом, и это включает int, char, long, long long, их варианты unsigned и перечисления (enum). Такие типы, как float и double, недопустимы. • При инициации могут быть переданы только целые константы, заданные во время компиляции. Это означает 100, 100+99, 1 << 3 и т. д. допустимы, поскольку их выражение вычисляется во время компиляции. Аргументы, которые вовлекают вызовы функции, наподобие abs(-120), недопустимы. В качестве аргумента шаблона типы float / double могут быть допустимы, если они могут быть преобразованы в целочисленный тип.
ОК, мы может инициировать шаблон класса Array так:
Array<int, 10> my_array;
И что же? Какова цель аргумента SIZE?
Дело в том, что в шаблоне класса Вы можете использовать этот non-type integer аргумент - везде, где возможно Вы использовали целое число. Это включает:
• Цель 1: присвоение статической переменной (поля данных) класса.
Примечание: первые две строки декларации класса не показаны, предполагается что они все находятся в теле класса.
Поскольку можно инициализировать статическую целую константу внутри декларации класса, мы можем использовать non-type integer аргумент.
• Цель 2: для указания значения по умолчанию для метода.
Примечание: хотя C++ также позволяет любой не константе быть параметром по умолчанию для функции, я указал на это только для иллюстрации.
voidDoSomething(int arg = SIZE);// Non-const может тоже быть аргументом по умолчанию...
• Цель 3: чтобы задать размер массива.
Это важный момент, и non-type integer аргумент часто используется для этого. Давайте реализуем шаблон класса class template Array, использующий аргумент SIZE.
private:
T TheArray[SIZE];
T задает тип массива, SIZE его размер (integer) - проще не придумаешь. Так как массив находится в области private класса, то нам нужно определить несколько методов / операторов.
// Инициализация по умолчанию (например значением 0 для int)
Наверняка у типа T должен быть конструктор по умолчанию и оператор присваивания. Я покажу их реализацию для шаблона функции в следующей части статьи.
Нам нужно также реализовать операторы для доступа к элементам массива. Один из них оператор установки элемента массива по индексу, и другой делает выборку значения по индексу (значение типа T):
Обратите внимание, что первая перегрузка (которая декларирована с const) является методом выборки/чтения (get/read), и в нем реализована проверка - допустим ли индекс для массива или нет, иначе будет возвращено значение по умолчанию для типа T.
Вторая перегрузка возвращает ссылку на элемент, который с помощью неё может быть модифицирован вызывающим кодом. Здесь уже нет проверки на допустимость индекса, поскольку возвращается ссылка, так что не может быть возвращен локальный объект (T()). Однако Вы можете проверить аргумент индекса, возвратить значение по умолчанию, или использовать assert и/или выбрасывать исключение (throw exception).
Давайте определим другой метод, который будет логически суммировать все элементы массива:
T Accumulate() const
{
T sum = T();
for(int nIndex =0; nIndex < SIZE; ++nIndex)
{
sum += TheArray[nIndex];
}
return sum;
}
Как Вы можете интерпретировать, это потребует оператор += для цели типа T. Также обратите на то, что возвращаемый тип будет T сам по себе. Так что когда делается инициация Array с некоторым классом строки, то для вызова += на каждой итерации будет возвращена скомбинированная строка. Если тип цели не имеет реализованного оператора += и Вы вызовете этот метод, то получите ошибку компилятора. В этом случае Вам нужно не делать его вызов, либо нужно реализовать перегрузку нужного оператора в целевом классе.
[Шаблон класса в качестве аргумента для шаблона класса]
Хотя эта тема довольно трудна для понимания, и вовлекает некоторые неоднозначности, я все-таки сделаю попытку кое-что прояснить.
Во-первых, вспомните отличия между шаблоном функции и функцией шаблона. Если нейроны помогли перенести правильную информацию в кэш Вашего мозга, то Вы вспомните, что функция шаблона является экземпляром шаблона функции. Если подсистема поиска Вашего мозга не отвечает, то пожалуйста перегрузите эту информацию заново!
Экземпляр шаблона класса является классом шаблона. Таким образом, для следующего шаблона класса:
template<classT1, classT2>
classPair{};
получится инициация вот этого класса шаблона:
Pair<int, int> IntPair;
Четко поймите, что IntPair не шаблон класса, не инициация для шаблона класса. Это объект определенного инициирования класса шаблона. Шаблон класса / инициация это Pair< int, int >, которая генерирует другой тип класса (Вы знаете, что это делает наш друг компилятор). В сущности это то, во что превратится шаблон класса после преобразования компилятором для этого случая:
classPair<int, int>{};
Есть, конечно, более точная формулировка понятия для класса шаблона, но это определение легче для понимания. Теперь давайте перейдем к сути дела. Что если Вы передадите в шаблон класса некоторый класс шаблона? Я имею в виду, что может означать следующий оператор?
Pair<int, Pair<int, int>> PairOfPair;
Предположим, что он допустим, но если это так, то что он означает?
Во-первых, он действительно полностью допустим. Во-вторых, он инициирует два класса шаблона:
• Pair< int, int > - A • Pair< int, Pair< int, int > > - B
Оба типа, и A, и B будут инициированы компилятором, и если в этом месте произойдет ошибка, вызванная любым из типов этих шаблонов класса, то компилятор сообщит об этом. Чтобы упростить это сложное инициирование, Вы можете сделать:
Обратите внимание, что второе поле в последних 2 строках это тип Pair< int, int >, так что он имеет тот же набор полей, к которым можно получить доступ. По этой причине поля first и second могут использоваться покаскадно.
Теперь Вы (надо надеяться) понимаете, что шаблон класса (Pair) принимает шаблонный класс (Pair< int, int >) в качестве аргумента, и на основе этого формируется конечная инициация.
Для этого обсуждения интересно рассмотреть инициацию, какая получится для Array из Pair. Вы знаете, что Pair принимает два аргумента типа для шаблона, и Array принимает один аргумент типа и аргумент SIZE (размер, целое число integer).
Array<Pair<int, double>, 40> ArrayOfPair;
Здесь int и double это аргументы типа для Pair. Следовательно, первый тип шаблона Array (помечен жирным шрифтом) это Pair< int, double >. Второй аргумент константа 40. Можете ли Вы ответить на вопрос: будет ли вызван конструктор Pair< int, double >? Когда он будет вызван? Перед тем, как Вы ответите, я просто сделаю инверсию инициации так:
Pair<int, Array<double, 50>> PairOfArray;
Ого! А вот это что могло бы означать?
ОК, это означает: PairOfArray является инициацией Pair, которая получает в первом типе int (для поля first), для типа second получит тип Array. Где Array (второй тип в Pair) будет 50 элементов типа double!
Не убивайте меня за это! Медленно досчитайте до 10, и поймите, что это просто базовые понятия шаблонов. Как только Вы получите четкое и ясное понимание всего этого, то полюбите шаблоны!
Прим. переводчика: теперь понятно, почему программисты не любят шаблоны...
Я использовал шаблонный класс (Array< double, 50 >) в качестве экземпляра для другого типа (Pair< int, ... >).
ОК, но что делает оператор сдвига вправо (>>)? На самом деле это не оператор, а просто завершение спецификации типа Array, за которой идет завершение типа спецификации Pair. Некоторые старые компиляторы требуют вставлять в этом месте пробел между двумя угловыми скобками, чтобы избежать ошибки.
Pair<int, Array<double, 50>> PairOfArray;
В настоящее время все современные компиляторы достаточно умны, чтобы понимать, что здесь используется окончание спецификации типа шаблона, и с ними о дополнительных пробелах волноваться не стоит. Так что Вы можете свободно использовать два или большее количество символов > для завершения спецификации (или спецификаций) шаблона.
Обратите внимание на то, что передача шаблонного класса (инициирование) не является чем-то особенным в терминах языка C++ - это просто тип, который может принимать шаблон класса.
И наконец, давайте рассмотрим примеры использования обоих объектов. Сначала - конструкторы.
Array< Pair<int, double>, 40> ArrayOfPair;
Это приведет к тому, что конструктор Pair будет вызван 40 раз, поскольку есть объявление массива постоянного размера в шаблоне класса Array:
T TheArray[SIZE];
который означал бы:
Pair<int, double> TheArray[40];
и следовательно, будет нужное количество вызовов конструктора для Pair.
Для следующего объекта:
Pair<int, Array<double, 50>> PairOfArray;
конструктор Pair проинициализирует 1 аргумент значением 0 (используя нотацию int()), и затем будет вызван конструктор Array (с нотацией Array()), как показано ниже:
Pair() : first(int()), second(Array())
{}
Поскольку конструктор по умолчанию шаблона класса Array предоставлен компилятором, то он и будет вызван. Если Вы не понимаете, о чем тут идет речь, то пожалуйста освежите Ваши знания C++.
Присвоение одного элемента ArrayOfPair:
ArrayOfPair[0] = Pair<int, double>(40, 3.14159);
Здесь Вы вызываете не константную версию оператора Array::operator[], которая возвратит ссылку на первый элемент Array (из TheArray). Элемент, как Вы знаете, это тип Pair< int, double >. Выражение в правой части оператора присваивания просто вызывает конструктор для Pair< int, double >, и передает ему требуемые аргументы. Присвоение завершено!
[Аргументы шаблона по умолчанию с шаблонами классов]
Сначала позвольте мне устранить любую двусмысленность в фразе 'Аргумент по умолчанию'. Та же фраза была использована в секции статьи про шаблон функции. В этом подразделе аргумент по умолчанию относится к параметрам самой функции, не к аргументам типа в шаблоне функции. Шаблоны функции, так или иначе не поддерживают аргументы по умолчанию для аргументов шаблона. Как отдельное примечание знайте, что методы шаблона класса могут брать аргументы по умолчанию, как их могут брать любая обычная функция / метод.
Шаблоны класса, с другой стороны, действительно поддерживают аргумент по умолчанию для типизованных / бестиповых (non-type) аргументов в параметрах шаблона. Рассмотрим пример:
template<classT, int SIZE=100>
classArray
{
private:
T TheArray[SIZE];
...
};
Я всего лишь изменил код около SIZE в первой строке шаблона класса Array. Второй параметр шаблона, целочисленная константа, теперь установлен в 100. Это означает, что когда Вы используете его так:
Array<int> IntArray;
то это в действительности означает:
Array<int, 100> IntArray;
что будет автоматически сгенерировано при инициации этого шаблона класса. Конечно, Вы можете указать свой размер массива явной передачей второго аргумента шаблона:
Array<int, 200> IntArray;
Помните о том, что когда Вы явно передаете параметр по умолчанию с тем же самым аргументом, заданным в декларации шаблона класса, это инициировало бы его только один раз. Этим я имею в виду, что следующие 2 объекта будут созданы для инициации только одного класса Array< int, 100 >:
Array<int> Array1;
Array<int, 100> Array2;
Конечно, если Вы поменяете параметр по умолчанию в определении шаблона класса на другое значение, не равное 100, это приведет к двум инициациям шаблона, поскольку у них будут разные типы.
Вы можете настроить аргумент по умолчанию путем использования const или #define:
constint _size =120;
// #define _size 150
template<classT, int SIZE=_size >
classArray
Наверняка использование символа _size вместо жестко закодированной константы будет означать то же самое. Однако использование символа упростило бы спецификацию по умолчанию. Независимо от того, как Вы определяете параметр шаблона по умолчанию для целого числа (который является бестиповым, non-type аргументом шаблона), это должна быть выражение константы, вычисляемое во время компиляции.
Вы обычно не использовали бы спецификацию по умолчанию для параметра non-type integer, если бы не использовали шаблоны для неких особенных вещей, наподобие meta-программирование, static-asserts SFINAE и т. п., которые поставляются как отдельная готовая часть. Чаще Вы увидите и будете реализовывать параметры по умолчанию для шаблонов класса, которые являются типами данных. Пример:
template<classT=int>
classArray100
{
T TheArray[100];
};
Это задает массив типа T размером 100. Здесь аргумент типа по умолчанию int. Это означает, что если Вы не указали тип при инициации Array100, то он будет основан на int. В следующем примере показано, как это использовать:
Array100<float> FloatArray;
Array100<> IntArray;
В первой инициации я передал float в качестве типа шаблона, и во второй я оставил все по умолчанию (для int), используя нотацию < >. Если Вы попытаетесь использовать шаблон класса так:
Array100 IntArray;
то это приведет к ошибкам компиляции, в которых говорится о необходимости задать параметры для Array100. Таким образом, Вы обязательно должны использовать пустые угловые скобки (< >) для инициации шаблона класса, если все аргументы шаблона используются по умолчанию, и Вас это устраивает.
Важно также помнить, что не шаблонный класс с именем Array100 нельзя будет использовать. Определение не шаблонного класса, наподобие как дано ниже, вместе с шаблонным классом (указанным выше или ниже), приведет к ошибке компилятора:
classArray100{};
// Array100 требует аргументы шаблона!
template<classT=int, int SIZE=100>
classArray
{
T TheArray[SIZE];
...
};
И наконец, оба тип и размер аргументов помечены по умолчанию как int и 100 соответственно. Четко поймите, что первый int для спецификации по умолчанию T, и второй int для не шаблонной спецификации константы. Для упрощения и улучшения читаемости вводите их на разных строках:
template<classT=int,
int SIZE=100>classArray{};
Теперь Вы можете с учетом новых знаний проанализировать значение следующих инициаций:
Точно так же, как в явном указании параметров в шаблонах функций, не разрешается указывать только завершающие аргументы шаблона. Следующая строка выдаст ошибку:
Array< , 400> IntArrayOf500; // ERROR
Как последнее замечание, действительно помните, что следующие два создания объекта инициируют только один шаблон класса, так как по существу они означают одно и то же:
[Принятие значения по умолчанию типа шаблона на другом типе]
Также можно задать значение по умолчанию типизованного / не типизованного (non-type) параметра на ранее поступившем параметре шаблона. Например, мы можем модифицировать класс Pair, чтобы второй тип был тот же самый, что и первый тип, если второй тип явно указан.
template<classType1, classType2= Type1 >
classPair
{
Type1 first;
Type2 second;
};
В этом модифицированном шаблоне класса Pair тип Type2 теперь по умолчанию будет такой же, как и тип Type1. Пример инициации:
Pair<int> IntPair;
как Вы можете догадаться, это то же самое, как:
Pair<int, int> IntPair;
но Вы избавлены от необходимости вводить второй параметр. Также можно задать умолчание и для первого параметра Pair:
template<classType1=int, classType2= Type1 >
classPair
{
Type1 first;
Type2 second;
};
Это значит, что если Вы не передадите никакого аргумента в шаблон, то Type1 будет типа int, и следовательно Type2 будет также int! Следующее использование:
Pair<> IntPair;
приведет к инициации следующего класса:
classPair<int, int>{};
Наверняка то же самое возможно и для non-type аргументов по умолчанию на другом non-type аргументе. Пример:
template<classT, int ROWS =8, int COLUMNS = ROWS >
classMatrix
{
T TheMatrix[ROWS][COLUMNS];
};
Однако зависимый параметр шаблона должен быть справа от того, от которого он зависит. Следующий пример выдаст ошибку:
template<classType1=Type2, classType2=int>
classPair{};
template<classT, int ROWS = COLUMNS, int COLUMNS =8>
classMatrix
[Методы класса и шаблоны функции]
Хотя эта тема не для абсолютных новичков, но здесь я коснусь и шаблоны функций шаблоны классов - проработка этой концепции логична для этого руководства.
Класс IntArray является простым, не шаблонным классом у которого есть массив целых чисел из 10 элементов. Но метод Copy разработан как шаблон функции (шаблон метода?). Он принимает один параметр типа шаблона, который выводится компилятором автоматически. Вот как мы можем его использовать:
Как Вы можете догадаться, метод IntArray::Copy был бы инициирован с типом float, поскольку мы передали в него массив чисел float. Чтобы избежать беспорядка, и лучше понять код, думайте просто об int_array.Copy только как об Copy, и об IntArray::Copy< float >(..) только как об Copy< float >(..). Шаблон метода класса это ни что иное, как просто обычный шаблон функции, встроенный в класс.
Заметьте, что везде использовал 10 в качестве размера массива. Интересно, что мы можем модифицировать класс так:
Это изменение улучшает реализацию класса IntArray и метода Copy в качестве кандидата для использования в программировании шаблонов!
Как Вы разумно предположили бы, метод Copy это всего лишь подпрограмма преобразования массива, которая преобразует int в любой тип везде, где возможно преобразование от int в указанный тип. Это один их допустимых случаев, где метод класса может быть написан как шаблон функции, беря свои же аргументы шаблона. Измените это шаблон класса, чтобы он мог работать для любого типа массива, а не просто для int.
Наверняка явное указание типа аргумента (explicit template argument specification) для этого шаблона метода также возможно. Рассмотрим другой пример:
С одной стороны, это обеспечивает хорошую гибкость, так как без написания дополнительного кода Convert может преобразовать себя (определенную инициацию) в любой тип данных - каждый раз, когда преобразование возможно на уровне компиляции. Если преобразование невозможно, как например из double в строковый тип, то будет выведена ошибка.
Но с другой стороны, это также вовлекает проблему непреднамеренной вставки ошибок. Вы можете не хотеть вызывать оператор преобразования, но он будет вызван (компилятор сгенерирует этот код), и Вы об этом не узнаете.
[Выводы]
Вы только что увидели проблеск силы и гибкости, который дают шаблоны. В следующей части будет рассмотрено больше усовершенствованных и интригующих возможностей. При разборе шаблонов изучайте их возможности последовательно, не перепрыгивая быстро от одного понятия к другому - тема все-таки непростая. Когда будете пробовать применить шаблоны в работе, не делайте это со старыми рабочими проектами, вместо этого испытывайте новую технологию в тестовых или только что разрабатываемых проектах.
Вот кратко о том, что было рассмотрено в этой статье:
• Чтобы избежать дублирования кода и проблем с его поддержкой, когда код по сути остается тот же, но работает с разными типами, мы можем использовать шаблоны. Шаблоны намного лучшее решение по сравнению с использованием макросов C/C++ или функций / классов, работающих поверх указателей на void. • Шаблоны не только безопасны с точки зрения типов, но они также уменьшают нежелательное увеличение размера кода, который на самом деле не используется (на основе этого кода компилятор не генерирует двоичный код). • Шаблоны функций используются, чтобы разместить код, который не является частью класса, и код является таким же / аналогичным для разных типов данных. В большинстве случаев компилятор автоматически определит нужный тип и сгенерирует соответствующий код. Иначе Вы должны определить тип, или Вы можете также сами явно указать тип. • Шаблоны класса делают возможным обернуть любой тип данных вокруг специфичной реализации. Это может быть массив, строка, очередь, список связей, потокобезопасная атомарная реализация чего-либо и т. п. Шаблоны классов действительно упрощают шаблонную спецификацию типа по умолчанию, что не поддерживается шаблонами функции.
Надеюсь, что Вам понравилось чтение, и оно сняло ментальную блокировку, что шаблоны это сложная вещь.
Сначала я начал читать про шаблоны в википедии, и уже думал, что шаблоны мне не по плечу. И потом нашел эту статью... Спасибо, и автору, и переводчику! теперь я рекомендую всем именно эту статью для знакомства с шаблонами!
Шикарная статья. До этого боялся даже к шаблонам подходить. Правда не до конца еще вкурил. Можно ли с помощью шаблонов сделать так что бы в структуре в одно из полей можно было записать как значение типа int так и значение типа String?
Комментарии
RSS лента комментариев этой записи