A megosztott tulajdonlás akkor indokolt, amikor egy erőforrás
élettartama nem köthető egyetlen lexikai hatókörhöz vagy egyetlen
tulajdonoshoz. Tipikus eset a több helyről elérhető, aszinkron
callbackben élő objektum, egy gráf csomópontja, vagy egy gyorsítótár
bejegyzése. Ilyenkor a std::shared_ptr több tulajdonost
enged, és az objektum akkor semmisül meg, amikor az utolsó tulajdonos
elengedi. Ez azonban nem alapértelmezés: ahol egyetlen tulajdonos
kijelölhető, ott a
[unique_ptr és egyedi tulajdonlás (21.3)](#s104)
egyszerűbb, olcsóbb és kiszámíthatóbb. A std::weak_ptr nem
tulajdonos, hanem olyan megfigyelő, amely biztonságosan meg tudja
kérdezni, hogy a kezelt objektum még él-e. A három eszköz együtt adja a
C++ determinisztikus erőforrás-kezelésének egyik fontos szeletét, de nem
oldja meg automatikusan a ciklusokat, a dangling nyers pointereket vagy
a szálbiztonságot.
A megosztott tulajdonlás szerződése szorosan épül a korábbi
objektum-élettartam-szabályokra: a shared_ptr destruktora
nem „szemétgyűjt”, hanem egy referenciaszámlálót csökkent, és az utolsó
erős hivatkozás megszűnésekor determinisztikusan lefuttatja a deletetert
vagy a destruktort. A
[Tároló újrafelhasználása és C++ objektumszabályok (20.6)](#s101)
részben tárgyalt objektumélettartam itt két külön élettartamra bomlik: a
kezelt objektuméra és a control blockéra.
A std::shared_ptr<T> egy másolható tulajdonosi
fogantyú. Két lényeges része van: a kezelt objektumra mutató pointer,
amelyet get() ad vissza, és egy control blockra mutató
pointer. A control block a shared_ptr létrehozásakor jön
létre, és nyilvántartja az erős (strong) és gyenge
(weak) hivatkozások számát, valamint a deletetert és az
allocatort. A control block konkrét típusa, mérete és elrendezése nem
része a nyelvi szerződésnek; a szabvány a megfigyelhető viselkedést írja
le, nem a belső bájtszerkezetet.
Amikor az utolsó erős shared_ptr megszűnik, a kezelt
objektum élettartama véget ér: lefut a destruktor vagy a egyedi deleter.
A control block azonban még életben marad, ha arra weak_ptr
mutat. A control block csak akkor szabadul fel, amikor a strong és a
weak hivatkozások száma is nulla. A weak számláló pontos belső értéke
implementációfüggő lehet; a lényeg az, hogy a weak_ptr ne
érje el a már megsemmisült objektumot, de a control blockot életben
tartsa.
| Esemény | Hatás a strong countra | Hatás a weak countra | Következmény |
|---|---|---|---|
shared_ptr másolása |
+1 | változatlan | új tulajdonos |
shared_ptr megszűnése |
-1 | változatlan | ha 0, az objektum megsemmisül |
weak_ptr másolása |
változatlan | +1 | a control block életben marad |
sikeres weak_ptr::lock() |
+1 | változatlan | új shared_ptr jön létre |
utolsó weak_ptr megszűnése |
változatlan | -1 | ha a strong is 0, a control block felszabadul |
A referenciaszámláló növelése és csökkentése atomikus, de ez csak a
számlálót védi. Ugyanazon shared_ptr példány egyidejű
módosítása továbbra is adatverseny, és a kezelt objektumot a
shared_ptr nem teszi automatikusan szálbiztossá. Ha az
objektumot több szál olvassa és írja, ahhoz külön szinkronizáció kell.
Az objektum megsemmisítése abban a szálban történik, amelyik az utolsó
erős hivatkozást elengedi; a többi szálnak ezt megfelelően
szinkronizálnia kell.
make_shared,
allokáció és weak tailA std::make_shared<T>(args...) egyetlen
allokációban hozza létre a control blockot és a T
objektumot. Ez gyorsabb lehet, mint a külön new T és a
külön control block-allokáció, és kivétel esetén is biztonságos: ha a
T konstruktora dob, a már lefoglalt tároló felszabadul, és
nem marad gazdátlan nyers pointer. A make_shared hátránya,
hogy nem adható meg hozzá egyedi deleter, és a T
konstruktorának elérhetőnek kell lennie.
A make_shared esetében az objektum és a control block
ugyanabban az allokációban van. Amikor az erős hivatkozások elfogynak,
az objektum destruktora lefut, de a memória csak akkor szabadul fel,
amikor az utolsó weak_ptr is megszűnik. Ezt nevezik weak
tail élettartamnak. Ha egy nagy objektumra sokáig élő
weak_ptr mutat, a nagy allokáció a látszólagos
megsemmisítés után is foglalva maradhat. Ilyen esetben lehet tudatosan
külön allokációt használni, de a legtöbbször a helyes megoldás a
weak_ptr élettartamának rövidre szabása.
A std::shared_ptr<T>(new T) külön allokációt
használ. Ez önmagában nem hiba, de két allokációt jelent, és a nyers
new egy nagyobb kifejezésben könnyen gazdátlanul maradhat,
ha a kiértékelés közben kivétel keletkezik. Ezért a tulajdonjog átadását
a lehető legközvetlenebbül, lehetőleg make_shared-del
érdemes megtenni.
shared_ptr és a double-control-block hibaA std::shared_ptr<T> nyers pointerből konstruálva
átveszi a tulajdonjogot, és új control blockot hoz létre. Ha ugyanarra a
nyers pointerre két külön shared_ptr-t építünk, két
független control block keletkezik, amelyek ugyanazt az objektumot
fogják felszabadítani:
// Tanítási példa: NE futtasd, nem definiált viselkedés.
auto raw = new Config{"localhost", 8080};
std::shared_ptr<Config> a(raw);
std::shared_ptr<Config> b(raw); // külön control block, dupla deleteA második konstruktor nem tud az első control blockjáról. A két
control block külön számlálja a tulajdonosokat, és mindkettő azt hiszi,
hogy ő szabadítja fel az objektumot. A következmény dupla felszabadítás,
azaz nem definiált viselkedés. Ugyanez a hiba, ha egy meglévő
shared_ptr-ből p.get()-tel készítünk újabbat,
vagy ha egy tagfüggvényben shared_ptr<T>(this)-t
írunk. A helyes megoldás a meglévő shared_ptr másolása vagy
mozgatása, illetve az enable_shared_from_this
használata.
Az aliasing constructor egy meglévő shared_ptr control
blockját használja, de a get() által visszaadott pointer
egy másik címre mutat:
struct Config {
std::string host;
int port;
};
auto cfg = std::make_shared<Config>(Config{"localhost", 8080});
std::shared_ptr<const int> port_view(cfg, &cfg->port);Itt a port_view erős tulajdonosa a teljes
Config objektumnak, miközben *port_view a
port tagra mutat. Amíg port_view él, a
Config sem semmisülhet meg, tehát a
cfg.reset() után is biztonságos lehet a
port_view használata. Fontos: az aliasing
shared_ptr nem non-owning view, hanem a teljes objektumot
tulajdonló, de a subobjectre mutató fogantyú. A mutatott subobjectnek a
tulajdonolt objektum élettartamán belül kell lennie. Ha a pointer olyan
objektumra mutat, amelynek élettartama rövidebb, mint a tulajdonolt
objektumé, a shared_ptr nem tudja megakadályozni a dangling
állapotot.
Az aliasing constructor hasznos például akkor, ha egy nagyobb
objektum egy tagját szeretnénk átadni egy API-nak, de a teljes objektum
életben tartását is biztosítani akarjuk. Az owner_before
összehasonlítás a control block tulajdonviszonya alapján rendez, nem a
get() cím szerint; ez segít például asszociatív
tárolóban.
weak_ptr,
lock és referencia-ciklusA std::weak_ptr<T> nem tulajdonos, hanem a control
blockra mutató gyenge hivatkozás. Nem növeli a strong countot, ezért nem
akadályozza meg az objektum megsemmisítését. A lock()
metódus vagy egy érvényes shared_ptr-t ad vissza, amely
biztosítja az objektum élettartamát a használat idejére, vagy üres
shared_ptr-t, ha az objektum már megsemmisült. A
lock() az egyetlen helyes „ellenőrzés és használat” minta;
az expired() vagy a use_count() külön
lekérdezése és utána a dereferálás versenyhelyzetet okozhat.
A leggyakoribb alkalmazás a referencia-ciklus megszakítása. Ha két
objektum shared_ptr-rel kölcsönösen hivatkozik egymásra,
egyik strong countja sem csökken nullára, ezért egyik sem semmisül meg.
Az egyik irányt weak_ptr-re kell cserélni. Az alábbi
példában a szülő erősen birtokolja a gyermeket, a gyermek viszont csak
gyengén figyeli a szülőt:
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Parent;
struct Child {
std::string name;
std::weak_ptr<Parent> parent; // nem tulajdonol
~Child() { std::cout << "Child " << name << " destroyed\n"; }
void report() const;
};
struct Parent {
std::string name;
std::vector<std::shared_ptr<Child>> children; // tulajdonol
~Parent() { std::cout << "Parent " << name << " destroyed\n"; }
};
void Child::report() const {
if (auto p = parent.lock()) {
std::cout << name << " parent=" << p->name << "\n";
} else {
std::cout << name << " parent expired\n";
}
}
int main() {
auto p = std::make_shared<Parent>();
p->name = "root";
auto c = std::make_shared<Child>();
c->name = "leaf";
c->parent = p; // weak hivatkozás
p->children.push_back(c); // erős tulajdonlás a szülő felől
std::cout << "parent strong=" << p.use_count()
<< " child strong=" << c.use_count() << "\n";
c->report();
p.reset(); // a szülő élettartama véget ér
c->report(); // a weak_ptr már expired
std::cout << "done\n";
}A laborban például ezzel a paranccsal fordítható:
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 -o cycle_demo cycle_demo.cppA példában a p és a c a tulajdonosok, a
Child::parent pedig nem tulajdonló view. A
p.reset() invalidálja a szülő objektumot; a
weak_ptr::lock() ezután üres shared_ptr-t ad,
ezért a report() az „expired” ágat futja. Ha a gyermekben
is shared_ptr<Parent> lenne, a két objektum
kölcsönösen életben tartaná egymást, és a p.reset() után
sem szabadulna fel egyik sem.
enable_shared_from_this inicializálási feltételeiAz std::enable_shared_from_this<T> egy
bázisosztály-sablon, amely lehetővé teszi, hogy egy már
shared_ptr által birtokolt objektum tagfüggvénye új
shared_ptr-t szerezzen anélkül, hogy új control blockot
hozna létre. A shared_from_this() egy meglévő ownerrel
megosztott shared_ptr-t ad vissza. Feltétele, hogy az
objektumot már egy shared_ptr tulajdonolja. C++17-től, ha
nincs ilyen owner, a hívás std::bad_weak_ptr kivételt dob;
korábbi szabványokban ez nem definiált viselkedés volt.
A bázis egy gyenge hivatkozást tárol, amely akkor inicializálódik,
amikor egy shared_ptr átveszi az objektum tulajdonjogát.
Ezért a konstruktorban még nincs érvényes owner: a
shared_from_this() hívása a konstrukció közben
bad_weak_ptr-hez vezet. Ugyanez a helyzet, ha az objektumot
stacken vagy unique_ptr-rel hozták létre, mert nincs
shared_ptr owner. Helyes minta: az objektumot
make_shared-del létrehozzuk, majd egy külön
init() lépésben, már létező owner mellett hívjuk a
shared_from_this()-t. C++17-től a
weak_from_this() is elérhető, ha nem akarunk azonnal erős
hivatkozást szerezni.
shared_ptr-t építeni,
például shared_ptr<T>(p.get())-tel. Ez double control
blockot és dupla felszabadítást okoz.shared_ptr<T>(this)-t írni
ahelyett, hogy enable_shared_from_this-t használnánk.shared_ptr-rel kölcsönös hivatkozást létrehozni,
majd csodálkozni, hogy a destruktorok nem futnak le. A ciklust
weak_ptr-rel kell megszakítani.weak_ptr esetén expired()-et vagy
use_count()-ot ellenőrizni, majd külön dereferálni.
Helyette egyszer kell lock()-olni, és a kapott
shared_ptr-t használni.shared_ptr szálbiztossá teszi a
kezelt objektumot. Csak a referenciaszámláló atomikus; az objektum
hozzáféréséhez külön szinkronizáció kell.shared_ptr-rel olyan subobjectre mutatni,
amelynek élettartama rövidebb, mint a tulajdonolt objektumé. A
shared_ptr a teljes objektumot tartja életben, nem a
mutatott részt.make_shared-del létrehozott nagy objektumra sokáig
életben tartott weak_ptr-t hagyni, mert a weak tail miatt a
teljes allokáció foglalva marad.1. feladat (megértés). Mi lesz a kimenet, és mennyi
a use_count() az egyes pontokon?
auto a = std::make_shared<int>(42);
std::weak_ptr<int> w = a;
auto b = a;
a.reset();
std::cout << b.use_count() << " " << w.expired() << "\n";
auto c = w.lock();
std::cout << (c ? *c : -1) << "\n";2. feladat (kódolás). Készíts egy
Subject–Observer mintát, ahol a
Subject shared_ptr-ekkel tárolja a
megfigyelőket, az Observer pedig weak_ptr-rel
hivatkozik a Subject-re. A Subject
értesítéskor hívja meg az observereket, az observer a
lock() segítségével olvassa a Subject
állapotát. Biztosítsd, hogy a Subject megsemmisülése után
az observer ne érjen érvénytelen objektumhoz.
3. feladat (diagnosztika és javítás). Az alábbi töredékek közül melyek hibásak, és miért? Javítsd őket.
auto p = std::make_shared<X>(); auto q = std::shared_ptr<X>(p.get());
struct X : std::enable_shared_from_this<X> { X() { shared_from_this(); } };
X x; auto p = x.shared_from_this();
auto cfg = std::make_shared<Config>(); std::shared_ptr<int> v(cfg, &cfg->port); cfg.reset(); use(*v);
1. megoldás. A w weak hivatkozás, ezért
nem növeli a strong countot. b = a után a strong count 2.
a.reset() után csak b tartja életben az
objektumot, így b.use_count() értéke 1, és
w.expired() hamis, azaz 0. A
w.lock() sikeres, új shared_ptr-t ad, amelynek
hatására a strong count átmenetileg 2 lesz; a *c értéke 42.
A kimenet tehát:
1 0
42
Az ellenőrzési feltétel: a c élettartama alatt az
objektum nem semmisülhet meg, és a c megszűnése után a
strong count visszaesik 1-re. Ha a b-t is elengeddük, a
w.expired() igazzá válik.
2. megoldás. Az alábbi példában a
Subject a tulajdonos, az Observer a
weak_ptr-rel csak megfigyel. A ciklus azért szakad meg,
mert az Observer nem tartja életben a
Subject-et.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Subject;
struct Observer {
std::string name;
std::weak_ptr<Subject> subject;
explicit Observer(std::string n) : name(std::move(n)) {}
void on_event() const;
};
struct Subject {
int value = 0;
std::vector<std::shared_ptr<Observer>> observers;
void notify() const {
for (const auto& o : observers) {
o->on_event();
}
}
};
void Observer::on_event() const {
if (auto s = subject.lock()) {
std::cout << name << " sees " << s->value << "\n";
} else {
std::cout << name << " subject gone\n";
}
}
int main() {
auto s = std::make_shared<Subject>();
auto o = std::make_shared<Observer>("o1");
o->subject = s;
s->observers.push_back(o);
s->value = 7;
s->notify();
s.reset();
o->on_event();
}A Subject erősen birtokolja az observereket, az
Observer gyengén figyeli a Subject-et. A
s.reset() után a Subject megsemmisül, mert a
weak hivatkozás nem tartja életben; az o viszont a lokális
shared_ptr miatt még él, és a subject.lock()
üres lesz. Az ellenőrzési feltétel: a Subject destruktora
lefut, az Observer nem ér el felszabadított
Subject-et, és a ciklus megszűnik.
3. megoldás. (a) Hibás: p.get() átadása
egy új shared_ptr-nek új control blockot hoz létre,
miközben p már tulajdonol. Ez double control block és dupla
felszabadítás veszélye. Javítás: auto q = p;.
Hibás: a konstruktorban még nincs shared_ptr owner,
ezért a shared_from_this() C++17-től
std::bad_weak_ptr-t dob. Javítás: a konstruktor ne hívja; a
make_shared után, külön init() lépésben
hívjuk.
Hibás: a stacken létrehozott x-et nem birtokolja
shared_ptr, ezért a shared_from_this() szintén
bad_weak_ptr-t dob. Javítás:
auto p = std::make_shared<X>(); auto q = p->shared_from_this();.
Helyes: az aliasing constructor a v számára a
cfg control blockját használja, ezért a
cfg.reset() után a Config életben marad, amíg
v él. A *v a port tagra mutat, és
a Config élettartama lefedi. Az ellenőrzési feltétel:
v.use_count() a létrehozás után 2, majd a
cfg.reset() után 1; a v megszűnésekor a
Config felszabadul.
A shared_ptr megosztott tulajdonlást ad: az utolsó erős
hivatkozás megszűnésekor determinisztikusan megsemmisíti a kezelt
objektumot. A control block élettartama ettől külön válik: a weak
hivatkozások életben tartják, ezért a make_shared
allokációja a weak tail miatt tovább foglalhat. A nyers pointerből
készített shared_ptr mindig új control blockot hoz létre,
ezért ugyanarra a pointerre csak egy tulajdonosi láncot szabad építeni.
Az aliasing constructor a teljes objektumot tartja életben, miközben egy
subobjectre mutat. A weak_ptr::lock() a biztonságos
ellenőrzés és használat eszköze, a referencia-ciklust pedig az egyik
irány weak hivatkozásra cserélésével kell megszakítani. Az
enable_shared_from_this csak akkor működik, ha az
objektumnak már van shared_ptr ownerje; konstruktorban,
stacken vagy unique_ptr mellett nem használható.