Днес ще бистрим тънките подробности относно вече загатнатата тема за шаблоните.
Какво е шаблон?
Функция или клас, която работи не с променливи от някакъв дефиниран тип, а с абстрактни променливи, се нарича шаблонна функция/клас. Когато някоя програма използва такава функция, тя трябва да изрично да уточни с какъв тип данни се използва. Един много прост пример - фунцкия, която намира по-голямата от две променливи.
#include<iostream>
using std::cout;
using std::endl;
template <class T>
T max(T a, T b)
{
return (a > b ? a : b);
}
int main()
{
cout<<max(3,6)<<" "<<max("Tri","Shest");
return 0;
}
Горната програма печата
> 6 Tri
(Не е написано using namespace std, защото в std вече има дефиниран оператор макс и двата щяха да си пречат.)
Тук се използва един клас, но нищо не ни спира да си направим:
template <class A, class B, class C>
void function(A first, B second, C third)
{
cout«first«" "«second«" "«third«endl;
}
За какво са нужни?
Има един вечен проблем в програмирането.
Как да работим по-малко?
Как да си свършваме работата с по-малко код? Същите задачи, но за по-малко време?
Всички умни хора са мързеливи - ако не бяха, щяха да си вършат работата по трудния начин. Някои програмисти са умни. Умните програмисти са мързеливи. Най-най-основният начин да пишем по-малко код и да гледаме повече смешни снимки на котки е да спазваме DRY принципа.
Don't Repeat Yourself. Ако пишеш един и същи алгоритъм два пъти, единият път е било загуба на време.
Винаги е по-хубаво, когато софтуерът не се повтаря. Това контрастира с хардуера, при който колкото повече - толкова повече. Така де, ако имаш два трактора, това е по-хубаво, отколкото ако имаш един трактор. Ама нямаш особена нужда от два учебника по Дискретна Математика, нали?1
Шаблоните са начинът, по който C++ се справя с проблема с повтарянето.
Горният код реализира алгоритъма за намиране на максимален измежду два елемента за всички съществуващи типове данни. За цели числа, нецели числа, цели неотрицателни числа, символи, низове, указатели. И каквото още има в C++. Но наистина удобното му е, че това ще важи и за всеки тип, който някога може да бъде написан. Точно така, ако утре си направиш клас желе и искаш да видиш кое е по-вкусно - малиново желе или желе от диня - ще можеш да го направиш, без никакъв проблем.
Точно така, използвайки шаблони, ще можеш да се ограничиш да пишеш алгоритъма на Дийкстра точно веднъж през целия си живот. Йей.
Каква е уловката?
Всичката магия става в компилатора. Той вижда как точно се използва шаблона - именно с кои типове ще се използва, и генерира (и компилира) код за всеки един отделен тип.
Ако искаш следният код да се изпълни:
Jelly Pink("Raspberry");
Jelly Green("Watermelon");
cout<<max(Pink, Green)<<endl;
То, трябва да е сигурно, че това ще се компилира:
Jelly max(Jelly a, Jelly b)
{
return (a > b ? a : b);
}
Защото компилаторът точно до това ще сведе кода на програмата. В крайна сметка, класът Jelly трябва да има дефиниран опреатор >. Логично. Това може би е очевидно, но същата логика важи за всички шаблони и всяка употреба на класовете:
template <class T>
int get_x(T item)
{
return T.x;
}
Ако T няма член променлива x - голяма работа, че програмата ти е с шаблон.
Добра практика е в такива програми да се използва основно стандартен интерфейс. Тоест, операторите, които ги има в C++ - плюс, минус, оператори за сравнение, за потоци. Точно така е направено и в стандартната библиотека на C++ (STL - Standart template library). Там всичко е направено с шаблони, а от класовете на програмиста се очаква да имплементират някои основни функции2.
Предимства спрямо макросите
Съществуват добре осведомени C програмисти, които биха попитали:
Добре де, ама защо просто горе не използвахме #define max(a,b) (a > b ? a : b)
Много причини. Ще илюстрирам с примери, а ти се опитай да разбереш какво не е наред с всеки един от тях.
#include<stdio.h>
#define max(a,b) ( a > b ? a : b )
#define max3(a,b,c) (max( max(a,b), c))
#define swap(a,b) if (a != b) { a+=b; b=a-b; a-=b;}
int main()
{
/* Зло номер едно */
int a = 5, b = 5, c = 5;
if (a <= b)
swap(a,b)
else
a = b*b*b;
printf("%d %d %d\n",a,b,c);
/* Зло номер две */
a = 5; b = 5; c = 5;
printf("%d\n",max3(++a, --b, c++));
printf("%d %d %d\n",a,b,c);
return 0;
}
Освен това, ако в някой момент програмата ти забие(т.е. когато програмата ти забие), много по-нервоспестяващо ще е да видиш нещо от рода на "Unhandled exception in swap(int a, int b)", отколкото "Unhandled exception in if (a != b)". А макрос ще ти изведе именно втората грешка.
Това не са всички проблеми, има и още.
Макросите, когато са употребени кадърно, са нещо полезно. Но като цяло са зло. Голямо зло. Особено когато работиш по някакъв проект с макроси и после някой друг го наследи. Ти знаеш точно какво ще ти счупи програмата. Ама новакът не знае. И не е негова вината, когато всичко гръмне.
Видове шаблони
Има два вида шаблони. Почти еднакво се използват, но единият се отнася до класове, другият до функции.
Функционални
Преди името на фунцкията се декларира какви шаблони ще се използват.
вместо типа на класа, се използва условното му наименование. Т.е. вместо int/char/…, например се пише T. Останалата част е като нормална функция.
template<class T>
int min(T a, T b)
{
return (a < b? a : b);
}
Класови
Преди декларацията на класа се казва какви шаблони ще се ползват. Останалото е както обикновено.
template<class A, class B>
class pair
{
A first;
B second;
public:
pair(A, B);
...
};
Преди дефиницията на всеки метод отново се указва, че иде реч за шаблони. Забележи тънкия момент, че се отбелязва на две места(гениално, знам).
template <class A, class B>
pair<A,B>::pair(A _first, B _second) : first(_first), second(_second)
{
cout<<"Pair successfully initialized with values "<<first<<" and "<<second<<endl;
}
Внимание. friend функции/класове с шаблони се правят ужасно, ужасно гадно. Адски трудно да се подкара веднъж, а оттам нататък - адски трудно за поддръжка. Най-добре измисли друг вариант.
Специализация
Възможно е в някои ситуации нормалните шаблони да не те устройват. Може за някой клас да е нужно специално поведение. Например класът не имплементира някой оператор или имплементацията не е такава, каквато е нужна за шаблона. В такива случаи се предлага специализация. Изрично се записва за кои класове важи следният шаблон, и той се ползва само в тези случаи.
Например, функция с шаблон, която сравнява два обекта. Ама искаш за низове вместо да ги сравнява лексикографски, да ги сравнява по дължина. Или просто да печата разни работи в специални случаи, каквото и да е.
template <>
pair<char *,char *>::pair(char * _first, char * _second) : first(_first), second(_second)
{
cout<<"Pair successfully initialized with values "<<first<<" and "<<second<<endl;
cout<<"Initalized with char * arguments"<<endl;
}
...
pair<char *, char *>("ONE","TWO");
Съжалявам, че примерът е скучен. Имах много по-як пример с функция, измерваща сладурщината на домашните животни и винаги връщаща максимум, ако животното е заек, но в крайна сметка примерът просто не се получи хубав. Oh well.
Ако искаш да си оплетеш програмата на възел, така че никой да не може да ти я прочете, използвай приятелски класове със специализирани шаблони. И полиморфизъм. И множествено наследяване, защото така.
Разлика между специализациите
Има малка разлика между специализация на функция и на клас.
Специализация на клас може да бъде и само частична, докато специализация на функция винаги е пълна.
Тоест, ако имаме:
template <class A, class B>
pair<A,B> ...
понеже работим с клас, следната глупост ще бъде напълно валидна:
template<class A, class B>
class pair<A, B*>
{
A first;
B* second;
public:
pair(A, B *);
};
Реално си дефинираме отделен клас за работа с указатели. На някои места е възможно двата класа да се припокриват, т.е. и при двата да се вика една и съща функция, иначе са си отделни. Може работещ код да се счупи когато нещо такова бъде въведено в програмата.
Това е висш пилотаж. На самолет без крила. Няма твърде голяма причина да се ползва.
template <class A, class B>
pair<A,B *>::pair(A _first, B *_second) : first(_first), second(_second)
{
cout<<"Pair successfully initialized with values "<<first<<" and "<<second<<endl;
cout<<"Special Pointer version used"<<endl;
}
Иначе си работи, де.
pair<int , char *>(5,"DSA");