1. Giriş
İlk dəfə shared_ptr-dən eşitdikdə beyin sevinərək belə nəticə çıxarır: “O! İndi obyektlərin ömrü barədə heç vaxt düşünməyə ehtiyac yoxdur!”. Bu, böyük soyuducu alıb ərzaqların son istifadə tarixini unutmağa bənzəyir. Hələ də düşünməli olacaqsınız — sadəcə problemlər daha baha və maraqlı olacaq.
Səbəb budur ki, shared_ptr bir işi həll edir: «obyekti nə vaxt silmək, əgər bir neçə sahib varsa». Amma o, sizin dizaynı avtomatik düzgün etmir. Üstəlik, shared_ptr-i qorxduğunuz hər yerə sürtməyə başlasanız, yeni növ səhvlər əldə edəcəksiniz: sahiblik dövrləri, gözlənilməz yaşayış müddətinin uzanması, proqnozlaşdırılmamış asılılıqlar və API, hansı ki kimin nə üçün cavabdeh olduğunu anlamaq çətin olur.
Buna görə layihə səviyyəsində razılaşma lazımdır: default olaraq unique_ptr istifadə edirik, shared_ptr-ə yalnız həqiqətən paylaşılmış sahiblik məntiqi olduqda keçirək. Və məna «mən daha rahatam» yox, «proqram tələblərinə görə əslində bir neçə müstəqil sahib var» olmalıdır.
“Shared_ptr-ə keçək ki, düşünməyək” — təhlükəli razılaşma
Tipik senarini təsəvvür edək: siz Task*-i xaricə qaytarırsınız, istifadəçi tapşırığı seçir, sonra storage tapşırığı silir — və həmin göstərici dangling olur. Yeni gələn adama belə fikir gəlir: “O! Shared_ptr etmək lazımdır, onda obyekt ona kimsə istinad etdiyi müddətcə yaşayacaq”.
Bəzən bu həqiqətən problemi həll edir, amma qiymət özü problemə nisbətən daha yüksək ola bilər. Çünki siz memarlığı dəyişdirdiniz: indi tapşırıqlar «harda isə kimsə saxlayırsa» yaşayar. Bu gözlənilməz təsirlərə gətirə bilər: tapşırığı storage-dan sildiniz, amma UI-də haradasa shared_ptr nüsxəsi qaldığından o hələ də yaşayır. Və istifadəçi sonra təəccüblənir: “Niyə o hələ də mövcuddur?”
Yəni shared_ptr — bu dangling pointer-ın “təmir”i deyil. Bu, obyektlərin həyat modelinə keçiddir. Bəzən lazım, bəzən isə təsadüfi seçilmiş.
2. Rol modellərinin sürətli xülasəsi: sahib, sahibləri bölən və müşahidəçi
Pointer seçməzdən əvvəl özünüzə bir sual verməyi öyrənin: bu istinad obyektə hansı rolu oynayır? Üç sözdən ibarət kifayətdir: sahib, sahibləri bölən və müşahidəçi.
Sahib ömürə cavabdehdir. O yaşadığı müddətcə obyekt mütləq yaşayır. Sahib öldükdə — obyekt məhv olunmalıdır (və ya sahiblik başqa sahibə ötürülməlidir). Bu, adətən unique_ptr-in təbii roludur.
Sahibləri bölən də ömürə cavabdehdir, amma belə şəxslər bir neçədir. Obyekt ən azı bir sahibləri bölən yaşadığı müddətdə mövcuddur. Bu, shared_ptr-dir.
Müşahidəçi “uğur qazanarsa” daxil ola bilər: obyekt hələ də varsa ona baxa bilər, amma onu canlı saxlamaq hüququ yoxdur. Bu, weak_ptr (və ya bəzən sahibsiz raw pointer/referans) roludur, amma bu dərsdə diqqətimizi shared_ptr/weak_ptr cütlüyünə saxlayırıq.
Bu modeli yalnız bilmək kifayət deyil — kodda tiplərə baxıb “oxumağı” bacarmaq vacibdir: sahənin və ya funksiyanın parametrinin tipi kiminsə sahib olduğunu göstərməlidir.
3. Qayda №1: unique_ptr ilə başlayın
Əgər obyektin bir aydın sahibi varsa, std::unique_ptr<T> demək olar ki, həmişə ən yaxşı seçimdir. Burada bir anda hər şey qalib gəlir: kodun anlaşılığı, ömrün proqnozlaşdırılması, paylaşılmış sahiblik dövrlərinin olmaması, daha az əlavə xərclər və asan debuq.
Praktikada unique_ptr — “sahib-pointer”dır, hansı ki birbaşa bildirir: “obyekt mənə məxsusdur, yalnız mənə”. Əgər sahibliyi ötürmək lazımdırsa — bunu açıq şəkildə std::move ilə edirsiniz. Bu çox yaxşıdır: kodda məsuliyyətin harda köçdüyü görünür.
Gəlin bunu kiçik tətbiq nümunəsində möhkəmləndirək. Tutaq ki, biz konsol “tapşırıq meneceri” yazırıq və tapşırıqları yaddaşda saxlamaq istəyirik. Ən sadə modeldə storage tapşırıqlara sahib olur.
#include <memory>
#include <string>
#include <vector>
struct Task {
int id{};
std::string title;
};
struct TaskStorage {
std::vector<std::unique_ptr<Task>> tasks;
};
Burada oxumaq asandır: TaskStorage bütün tapşırıqların sahibidir. Heç kim başqa «default olaraq» tapşırığa sahib olmamalıdır. Deməli, tapşırığın həyatı aydındır: o tasks-də olduğu müddətcə mövcuddur.
Obyekti sahibliyi vermədən necə vermək
Unique_ptr olduqda tez-tez sual yaranır: “Onda obyekti xaricə necə vermək lazımdır?”. Yeni gələn bəzən findTask()-dən unique_ptr qaytarmaq istəyir — amma o zaman storage obyektə sahibliyi itirəcək. Adətən axtarış üçün bu istənilən nəticə deyil.
Çox vaxt düzgün cavab: storage sahibdir, xaricə isə sahib olmayan giriş verilir. Məsələn, raw pointer Task* (null ola bilər kimi), və ya referans Task& (əgər mövcudluğu zəmanət verilirsə), və ya oxumaq üçün const Task* / const Task&.
Nümunə: tapşırığı axtarırıq və sahib olmayan pointer qaytarırıq. Diqqət: bu «raw ownership qaytarmaq» deyil, «baxmağa icazə vermək»dir.
#include <memory>
#include <vector>
Task* find_task(std::vector<std::unique_ptr<Task>>& tasks, int id) {
for (auto& p : tasks) {
if (p->id == id) return p.get(); // sahib olmayan pointer
}
return nullptr;
}
Burada vərdiş vacibdir: .get() — bu, əslində «mənə raw ünvan ver, amma sahibliyi özündə saxla». Və əgər nə etdiyinizi bilirsinizsə, bu normal əməliyyadır.
4. Shared_ptr həqiqətən nə vaxt məntiqlidir
std::shared_ptr o halda məna daşıyır ki, proqram məntiqinə görə bir neçə müstəqil komponent var və onların hər biri deyə bilər: «mən işlədiyim müddətcə obyektin mütləq yaşamasını istəyirəm».
Açar söz — müstəqil. “Mən ləzzət üçün belə edirəm”, “məndən qorxuram” yox, məhz “sistem tələblərinə görə obyekt proqramın hissələri arasında hərəkət etdikdə mərkəzi sahiblik olmadan yaşamalıdır”.
Sadə həyat nümunəsi (çətin framework-lərsiz): sizdə «Tətbiq parametrləri» obyekti var və onu müxtəlif modullar istifadə edir. Əgər bu modullar müstəqil yaşayır və biri digərindən uzun qazana bilərsə, bəzən parametrlərə bölüşülən sahiblik əlverişli olur. Amma belə hallarda da tez-tez tək bir sahibə const& ötürmə ilə kifayət etmək olar — sadəcə sahib aydın müəyyən edilməlidir.
shared_ptr-ə daha «dürüst» nümunə: hesablama nəticəsinin cache-lənmiş obyekti, hansı ki proqramın bir neçə hissəsinə verilə bilər və hər hissə istifadə etdiyi müddətcə nəticənin məhv edilməməsini təmin etmək istəyir.
#include <memory>
#include <string>
struct Report {
std::string text;
};
std::shared_ptr<Report> make_report() {
auto r = std::make_shared<Report>();
r->text = "Weekly report";
return r; // sahibliyin payını xaricə verir
}
Shared_ptr-i dəyər üzrə qaytarmaq oxunan kontraktdır: “mən obyekt yaradıram və onu paylaşılmış sahiblikdə verirəm”.
Shared_ptr niyə göründüyündən bahadır
shared_ptr-in əlavə xərcləri yalnız performans deyil. Bəli, orada kontrol bloku, sayğac üçün atomik əməliyyatlar (çox implementasiyalarda), əlavə allocasiyalar və instruktsiyalar var. Amma daha ağrılı olan başqa şeydir: proqram haqqında düşünmə xərci artır.
unique_ptr-lə demək olar ki, həmişə cavab verə bilirsiniz: “obyektə kim sahibdir?” — sahəyə baxaraq. shared_ptr-lə cavab tez-tez belə olur: “bilmirəm… kimsə”. Və sonra axtarış başlayır: bütün nüsxələri tapmaq, niyə obyekt hələ də yaşayır və niyə destruktor çağırılmayıb.
Əlavə detal: use_count() qurtuluş halqası kimi görünə bilər (“indi baxım, neçə sahib var!”), amma real kodda bu üzərində etibar ediləcək yaxşı vasitə deyil. use_count()-in zəmanətləri zəifdir və ona görə çoxsaplı və incə ssenarilərdə düzgünlüyü ona əsaslanmaq olmaz; bu daha çox diaqnostik alətdir.
Buna görə shared_ptr güclü alətdir, lakin onu hər gün cibdən çıxarmaq əvəzinə «alətlər qutusundan» lazım olduqda götürmək daha yaxşıdır.
5. weak_ptr: “istina ehtiyac var”, amma sahiblik yox
Əgər artıq shared_ptr-iniz varsa, dərhal sual yaranır: “Başqa obyektdən istinad etmək istəyirəm, amma sahiblik dövrü yaratmaq istəmirəm — nə etmək lazımdır?”. Cavab — weak_ptr.
Seçim qaydaları nöqteyi-nəzərindən weak_ptr üçüncü variant kimi yox, shared_ptr-ə tamamlayıcıdır. O, naviqasiyanı qurmaq istədikdə ortaya çıxır (məsələn, iki yönlü siyahıda “geri” və ya düyünün “valideyn”i), amma bu əlaqəni sahib edən etmək istəmirsinizsə.
Yaxşı nümunə: “weak_ptr saxla, istifadə zamanı bir dəfə lock() et və alınan shared_ptr-lə işləyin”.
#include <memory>
struct Task;
struct Link {
std::weak_ptr<Task> selected;
};
struct Task {
int id{};
};
Burada Link Task-ı canlı saxlamır. O sadəcə xatırlayır: “əgər o tapşırıq hələ də mövcuddursa — mən ona daxil ola bilərəm”.
6. Seçim yaddaşı: qayda, cədvəl və mini-sxem
Əgər obyektin bir aydın sahibi varsa (konteyner, menecer, “storage”, valideyn obyekt) — unique_ptr istifadə edin. Kiminsə baxması lazımdırsa — T*, T&, const T*, const T& verin (kontrakta görə).
Əgər dizaynda bir neçə müstəqil sahib lazımdırsa və sadə mərkəzləşdirilmiş sahiblik qurmaq mümkün deyilsə — onda shared_ptr.
Əgər shared_ptr dünyasında “istifadə etmək amma sahib olmaq istəmirəm” disiplini lazımdırsa — weak_ptr.
Yadda saxlamaq asan olması üçün qısa seçim cədvəli saxlayın.
| Koda uyğun vəziyyət | Nəni zəmanət etmək istəyirsiniz | Tipik seçim |
|---|---|---|
| “Obyekt konteyner/storage-a məxsusdur” | Aydın tək sahib | |
| “Sahibliyi ötürmək lazımdır” | Yeni sahib məsuliyyəti götürür | |
| “Bir neçə proqram hissəsi obyektin canlı qalmasını təmin etməlidir” | Obyekt heç olmasa kiməsə lazım olduğu müddətcə yaşayır | |
| “İstinadı xatırlamaq lazımdır, amma ömrünü uzatmaq yox” | Yalnız obyekt hələ də yaşayırsa daxil olmaq | |
Qərarvermə mini-sxemi
Hər dəfə smart pointer görəndə “zen tutmaq” məcburiyyətində qalmamaq üçün qısa qərar alqoritmi ağlınızda saxlamaq faydalıdır:
flowchart TD
A["Saxlamaq/obyektə istinad vermək lazımdır"] --> B{"Ömrün sahibi kimdir?"}
B -->|Bir aydın sahib| C["unique_ptr sahibin yanında"]
C --> D["Xaricə: T*/T&/const T& (müşahidə)"]
B -->|Bir neçə müstəqil sahib| E["shared_ptr sahiblərdə"]
E --> F{"Geri/tsiklik əlaqələr varmı?"}
F -->|Bəli| G["Qır: weak_ptr bir tərəfdə"]
F -->|Yox| H["shared_ptr lazım olan yerlərdə"]
7. Shared_ptr əvəzinə ID saxlamaq daha yaxşı olan hallarda
Praktik məsələ: bəzən biz shared_ptr-ə tərəf uzanırıq təkcə ona görə ki, uzunömürlü istinad saxlayırıq və obyekt yox ola bilər.
Belə hallarda çox vaxt düzgün olan pointer deyil, identifikator saxlamaqdır (məsələn, int id) və lazım olduqda hər dəfə axtarış etmək. Bəli, biraz əlavə kod, amma gizli həyatın uzanması yoxdur və “niyə bu hələ də yaşayır” sualı yaranmır.
Task tətbiqimiz üçün mini-ideya: UI selected_id saxlayır, shared_ptr<Task> deyil.
#include <optional>
struct UiState {
std::optional<int> selected_task_id;
};
Sonra göstərərkən və ya redaktə edərkən find_task(..., *selected_task_id) çağırırsınız və dürüstcə nullptr-u emal edirsiniz. Bu bəzən tətbiqin sahiblik modelini dəyişməkdən daha asandır.
8. Tipik səhvlər
Səhv №1: shared_ptr-i default olaraq istifadə etmək, çünki bununla rahatdır.
Bu çox insanın almasınacağı tələkdir: hər şeyi “təhlükəsizlik kaskası” ilə qorumaq istəyirsən. Amma shared_ptr kaska deyil, daha çox ağır zireh kimidir: onu geyinə bilərsiniz, amma tez yorulacaqsınız və bəzən qapıdan keçə bilməyəcəksiniz. Əgər obyektin bir sahibi varsa, unique_ptr ömrü daha sadə və dürüst edir.
Səhv №2: dangling pointerları shared_ptr-ə keçməklə müalicə etməyə çalışmaq, amma modeli dəyişməmək.
Bəzən problem paylaşılmış sahiblərin olmaması deyil, saxlanılanın yanlış olmasıdır (məsələn, konteyner elementinə istinad saxlayırsınız, sonra onu silirsiniz). Çox vaxt id saxlayıb obyektləri yenidən tapmaq shared_ptr etməklə “əbədi yaşatma”dan daha yaxşıdır.
Səhv №3: iki tərəfli əlaqələrdə shared_ptr saxlamaq və sahiblik dövrü almaq.
İki tərəfli əlaqələr (“valideyn–uşaq”, “prev–next”, “a istinad edir b və b a”-nın olduğu yerlərdə) dövr riskini demək olar ki, avtomatik yaradır. Müalicə reset çağırmaq deyil, əlaqənin rolunu dəyişməkdir: bir tərəf weak_ptr olmalıdır.
Səhv №4: proqram məntiqini use_count()-ə əsaslandırmaq.
use_count() debug və təlim üçün faydalı ola bilər, amma proqramın düzgünlüyü üçün pis əsasdır: sahiblərin sayı dəyişə bilər və mexanizmin zəmanətləri məhduddur. Ona diaqnostika kimi baxın, yoxsa “əgər sahiblər 1-disə” kimi şərtlər qurmayın.
Səhv №5: funksiyaların signaturalarında «müşahidə» ilə «sahiblik»i qarışdırmaq.
Əgər funksiya std::shared_ptr<T>-i dəyər ilə qəbul edirsə, o, sahiblərdən biri olur — hətta əgər siz sadəcə obyektin çapını etmək istəyirdinizsə belə. Əgər sahiblik lazım deyilsə, const std::shared_ptr<T>& (shared-sahibliyin faktı vacib olanda) və ya ümumiyyətlə const T& (yalnız verilənlərə çıxış vacibdirsə) ötürün. Signatura nə qədər dürüstdürsə, həyat müddəti ilə bağlı sürprizlər bir o qədər az olar.
GO TO FULL VERSION