1. Sinif şablonu
Əgər bu ana qədər sakitcə Task, User, std::vector<Task> və ətrafında bir neçə funksiya yaradırdınızsa — təbrik edirəm, siz artıq proqramçısınız: eyni şeyi fərqli üsullarla təkrarlamağı öyrənmisiniz. İndi isə eyni şeyi bir üsulla təkrarlamağı öyrənəcəyik. Sinif şablonu o zaman lazım olur ki, sizdə belə bir hiss yaransın: “Mən eyni sinfi yazıram, sadəcə içindəki tip dəyişir”.
Təsəvvür edək ki, TaskPilot adlı konsol tətbiqimizi inkişaf etdiririk (mini tapşırıq planlayıcısı). Bizdə tapşırıqlar, layihələr, qeydlər, hətta “su içməyi xatırladan bildirişlər” var — və hamısında oxşar əməliyyatlar olur: siyahını saxlamaq, əlavə etmək, axtarmaq, çap etmək.
Əgər hələlik buna ehtiyacınız olduğuna tam əmin deyilsinizsə — bu normaldır. Vacib bacarıq yalnız “şablonu necə yazmaq” deyil, həm də “o, nə vaxt həqiqətən yerinə düşür” sualını anlamaqdır. Sinif şablonu güc verir, amma qarşılığında səliqə istəyir.
Sinif şablonu çertyoj kimi
Düşüncədə yanılmaq və Box-un (və ya Repository-nin) artıq hazır tip olduğunu sanmaq çox asandır. Əslində Box <...> olmadan — bu, “fincan” sözünə bənzəyir, amma hansı fincan olduğu bilinmir: keramika? kağız? üstündə pişik şəkli olan? Kompilyatora detallar lazımdır. Sinif şablonu hazır qəlibdir, konkret tip isə yalnız şablon arqumenti yerinə qoyulandan sonra yaranır.
Gəlin terminologiyanı rəsmiləşdirək, amma darıxdırıcı olmadan.
| Termin | Necə görünür | Sadə dillə nə deməkdir |
|---|---|---|
| Şablon parametri | |
“İçəridə naməlum T tipi var, sonra yerinə qoyacağıq” |
| Şablon arqumenti | |
“T əvəzinə konkret int tipini qoyduq” |
| İnstansiasiya | |
“Kompilyator int üçün sinfin konkret versiyasını yaratdı” |
Beyin qızışmasın deyə, kiçik sxem:
flowchart TD
A["template <typename T> class Box { ... }"] --> B["Box<int>"]
A --> C["Box<std::string>"]
A --> D["Box<double>"]
B --> E["Obyekt: Box<int> x;"]
C --> F["Obyekt: Box<std::string> s;"]
Box<int> və Box<std::string> — fərqli tiplərdir, eynilə int və std::string kimi. Formaca oxşardırlar, amma bir-biri ilə uyğun deyillər.
2. Nümunə: Box<T> və Setting<T>
Box<T>: “dəyər üçün qutu”
Mürəkkəb arxitekturadan başlamayaq deyə, oyuncaq, amma faydalı bir nümunə ilə başlayaq — Box<T>. Bu, T tipindən bir dəyər saxlayan sinifdir. Nümunə “həddən artıq sadə” görünə bilər, amma mexanikanı çox yaxşı göstərir: harada template yazmaq lazımdır, harada <T>, və const-metodların nə olduğunu.
Tutaq ki, TaskPilot-da biz müvəqqəti olaraq “hazırda seçilmiş layihə”ni (sətir) və “hazırkı prioritet filtri”ni (ədəd) saxlamaq istəyirik. İstənilən tip üçün qutu yarada bilərik.
#include <string>
template <typename T>
class Box {
public:
Box() = default;
private:
T value_{};
};
Hazırda bu, demək olar ki, faydasızdır: dəyər gizlədilib, metod yoxdur. Amma əsas məqam artıq görünür: T value_{}; — tipi T-dən asılı olan sahədir.
Minimal interfeys əlavə edək: dəyəri qoymaq və almaq. Və dərhal əvvəlki günlərdən olan biliyi tətbiq edək: oxumaq üçün çox vaxt const T& rahat olur ki, böyük obyektləri kopyalamayaq.
template <typename T>
class Box {
public:
explicit Box(T value) : value_(value) {}
const T& get() const { return value_; }
void set(T value) { value_ = value; }
private:
T value_{};
};
İndi main()-də bu belə görünür:
#include <iostream>
#include <string>
int main() {
Box<int> priority{2};
Box<std::string> project{"Study"};
std::cout << priority.get() << '\n'; // 2
std::cout << project.get() << '\n'; // Study
}
Diqqət edin: tip açıq şəkildə göstərilir — Box<int>, Box<std::string>. Funksiyalardan fərqli olaraq, burada kompilyator çox vaxt T-ni özü çıxara bilmir; siniflər üçün bazis variantda adətən tipi yazmaq lazımdır (sinif tipi üçün avtomatik çıxarış barədə kursda sonra danışacağıq, amma bu gün yox).
Setting<T>: daha az copy-paste və vahid davranış kontraktı
İnsanlar ilk dəfə Box<T>-i görəndə çox vaxt düşünürlər: “Əgər sadəcə int priority; və std::string project; yaza bilirəmsə, buna nə ehtiyac var?” Və bu, tam haqlı sualdır.
Fayda o zaman görünür ki, şablon sinifdə ümumi davranış yaranır, yəni fərqli tiplər üçün eyni olan davranış. Məsələn, biz TaskPilot-da “ayar dəyəri” saxlamaq və onu eyni formatda çap etmək istəyirik: açar = dəyər.
Setting<T> yaradaq — bir ayar.
#include <string>
#include <iostream>
template <typename T>
class Setting {
public:
Setting(std::string name, T value) : name_(std::move(name)), value_(value) {}
void print() const {
std::cout << name_ << " = " << value_ << '\n'; // T üçün operator<< tələb olunur
}
private:
std::string name_;
T value_;
};
İstifadə:
int main() {
Setting<int> maxTasks{"max_tasks", 50};
Setting<std::string> username{"user", "Alice"};
maxTasks.print(); // max_tasks = 50
username.print(); // user = Alice
}
Bax burada “əməliyyatlar üzrə kontrakt” ortaya çıxır, funksiya şablonlarında olduğu kimi: əgər biz std::cout << value_ edirik, deməli T tipi axına çap olunmağı bacarmalıdır.
Şablon sinif bizə vahid kod verdi: iki ayrı sinif IntSetting və StringSetting əvəzinə — bir dənə Setting<T>.
4. Praktik nümunə: Repository<T>
Oyuncaq nümunələr bir yana, indi həqiqi koda bənzəyən bir şey edək. TaskPilot-da bizdə mahiyyətlər var: tapşırıqlar, layihələr, qeydlər. Çox vaxt onlar std::vector-da yaşayır və biz belə əməliyyatlar edirik: əlavə etmək, id üzrə tapmaq, çap etmək.
Əgər ayrıca TaskRepository, ProjectRepository, NoteRepository yazsanız, bir anda görəcəksiniz ki, sizdə üç sinif var və onlar... demək olar ki, heç nə ilə fərqlənmir. Məhz burada şablon həqiqətən yerinə düşür.
Əvvəlcə tapşırığın sadə modelini sabitləyək (kursun əvvəlki günlərində artıq ona söykənmişdik):
#include <string>
struct Task {
int id{};
std::string title;
bool done{};
};
İndi isə şablon-repozitoriya. O, std::vector<T> saxlayır. Biz hesab edəcəyik ki, T-nin id adlı int tipli sahəsi var. Bu vacib məqamdır: şablon “bütün tiplər üçün möcüzəli” deyil, “kontrakt baxımından oxşar olan tiplər üçün universaldır”.
#include <vector>
template <typename T>
class Repository {
public:
void add(T item) {
items_.push_back(std::move(item));
}
const T* findById(int id) const {
for (const auto& x : items_) {
if (x.id == id) return &x;
}
return nullptr;
}
private:
std::vector<T> items_;
};
Task ilə istifadə:
#include <iostream>
int main() {
Repository<Task> tasks;
tasks.add(Task{1, "Learn templates", false});
tasks.add(Task{2, "Drink water", true});
const Task* t = tasks.findById(2);
if (t) std::cout << t->title << '\n'; // Drink water
}
Biz çox vacib bir effekt aldıq: bir saxlama və axtarış kodu fərqli mahiyyətlər üçün işləyir. Əgər sabah belə bir şey yaransa:
struct Project {
int id{};
std::string name;
};
onda Repository<Project> dərhal işləyəcək — sinfi kopyalamadan.
Praktik nümunə: TaskPilot-da tapılmış mahiyyətin çapı
Mövzu “boşluqda” qalmasın deyə, gəlin repozitoriyaya tapşırığı çap edən kiçik bir funksiya əlavə edək. Çap özü şablon deyil, amma şablon sinfin tətbiqə necə xidmət etdiyini görməyə kömək edir.
Task üçün adi çap funksiyası yazaq:
#include <iostream>
void printTask(const Task& t) {
std::cout << "#" << t.id << " " << t.title;
std::cout << (t.done ? " [done]\n" : " [todo]\n");
}
İndi findById nəticəsindən istifadə edək:
int main() {
Repository<Task> tasks;
tasks.add(Task{1, "Learn templates", false});
if (const Task* t = tasks.findById(1)) {
printTask(*t); // #1 Learn templates [todo]
}
}
Şablon sinif “tipik saxlama mexanikasını” bağlayır, biznes-kod isə (tapşırıqların məntiqi) adi və oxunaqlı qalır.
5. Sinif şablonunu nə çətinləşdirir
Sinif şablonu İsveçrə bıçağı kimidir. Çox faydalıdır, ta ki onunla konserv bankasını açmağa qərar verib bıçağın tiyəsini özünüzə tərəf tutana qədər. Çətinliklər demək olar ki, həmişə “fəlsəfədə” deyil, konkret yerlərdə olur: sintaksis, tiplərin uyğunsuzluğu, kompilyator mesajları.
Fərqli spesializasiyalar — fərqli tiplər
Bu, aşkar görünür, amma beyin mütəmadi olaraq “enerjiyə qənaət” etməyə çalışır və onları eyni hesab edir.
Repository<Task> tasks;
Repository<Project> projects;
// tasks = projects; // xəta: fərqli tiplər
Bu, bug deyil, özəllikdir: kompilyator sizi məntiqi səhvlərdən qoruyur.
Şablon adı <...> olmadan tip deyil
Yeni başlayanlar belə yazırlar:
Repository repo; // xəta: T göstərilməyib
Kompilyator nəyi saxlamaq lazım olduğunu təxmin edə bilmir. Düzgün variant:
Repository<Task> repo;
Bəli, bu, uzundur. Amma dürüstdür.
Şablonun içində siz dolayı şəkildə T üçün tələblər formalaşdırırsınız
Repository<T>::findById-də biz x.id yazdıq. Deməli, T tipində .id olmalıdır. Əgər siz belə etsəniz:
Repository<int> bad;
onda kompilyator id tələb olunan metodu kompilyasiya etməyə çalışanda “partlayacaq”. Bu, normal modeldir: şablon “boşluqda” deyil, “istifadə faktı üzrə” kompilyasiya olunur.
Hələlik şablon xətalarının dərin oxunuşuna getmirik, amma bir vərdiş çox faydalıdır: şablon yazanda öz-özünüzə “T nəyi bacarmalıdır?” sualını verin.
Sinifdən kənardakı metodlar: sintaksis daha uzun olur
Əgər metodların təriflərini sinif gövdəsindən kənara çıxarmaq qərarına gəlsəniz (adi siniflərdə etdiyiniz kimi), yazılış daha “rəsmi” görünür. Koda batmamaq üçün kiçik sinif üzərində minimal nümunə göstərəcəyəm.
template <typename T>
class Box {
public:
void set(T value);
private:
T value_{};
};
template <typename T>
void Box<T>::set(T value) {
value_ = value;
}
Burada dərhal iki “əlavə çətinlik” var: template <typename T>-ni təkrar yazmaq lazımdır, üstəlik Box<T>:: də yazılmalıdır. Kiçik nümunələrdə bu, sadəcə əsəbiləşdirir. Böyük nümunələrdə isə intizam yaradır.
6. Sinif şablonları ilə iş zamanı tipik səhvlər
Səhv №1: şablonu hazır tip kimi qəbul etmək.
Çox vaxt Box b; və ya Repository repo; yazırlar və təəccüblənirlər ki, kompilyator narazıdır. Şablon hazır qəlibdir. Tip yalnız arqument yerinə qoyulandan sonra yaranır: Box<int>, Repository<Task>.
Səhv №2: “hər ikisi Box-dur” deyə Box<int>-in Box<double> ilə uyğun olduğunu gözləmək.
Sinif şablonunda hər arqument ayrıca tip yaradır. Bu, “tənzimləməsi olan bir sinif” deyil, “siniflər ailəsi”dir. Ona görə də fərqli spesializasiyalar arasında mənimsətmə qadağandır və bu, məntiqin qarışmasının qarşısını alır.
Səhv №3: təsadüfən şablona T-nin dəstəkləmədiyi əməliyyatı əlavə etmək.
Məsələn, Setting<T>::print()-də biz operator<< istifadə edirik. int və std::string üçün bu normaldır, amma öz tipiniz üçün — yox, ta ki siz çap imkanını təmin edənə qədər. Şablon kodu yalnız onu istifadə etdiyiniz anda kompilyasiya olunmağa başlayır, buna görə də xəta gözlənilmədən — elan yerində yox, tətbiq yerində “çıxa” bilər.
Səhv №4: konteyner elementinə göstərici/istinad qaytarmaq və sonra onu uzun müddət haradasa saxlamaq.
findById nümunəsində biz const T* qaytardıq, özü də std::vector daxilindəki elementə. Bu rahatdır, amma təhlükəlidir: əgər göstəricini saxlayıb sonra add() etsəniz (vektor yaddaşı yenidən bölə bilər). Yeni başlayan üçün təhlükəsiz vərdiş: tapılmış göstəricidən dərhal istifadə etmək, “burada və indi”, onu uzunömürlü dəyişən kimi saxlamamaqdır.
Səhv №5: kompilyatorun uzun mesajlarına görə panikaya düşmək.
Şablon sinif tipə “uyğun gəlməyəndə” xətalar çox vaxt böyük mətn zolağı kimi görünür. Bu, ümumiləşdirilmiş kodun adi qiymətidir. Düzgün yanaşma — ilk məzmunlu sətri axtarmaqdır: kompilyator sizin T-niz üçün hansı əməliyyatı yerinə yetirə bilmədi.
GO TO FULL VERSION