Přeskočit na obsah

shared_ptr, slabý_ptr a grafy vlastnictví

Pochopte sdílené vlastnictví počítané s referencí pomocí shared_ptr, cykly přerušení pomocí slabé_ptr, naučte se o řídicích blocích a aliasingu a zjistěte, kdy je shared_ptr špatnou volbou.

shared_ptr: Sdílené vlastnictví

std::shared_ptrumožňuje sdílené vlastnictví — více instancíshared_ptrmůže vlastnit stejný objekt. Objekt je zničen, když je zničen nebo resetován poslední vlastnícíshared_ptr.

shared_ptrinterně udržuje řídící blok obsahující:
- silný počet referencí (počet vlastníkůshared_ptr)
- slabý počet referencí (početweak_ptrpozorovatelů)
- Mazač a alokátor

shared_ptrje větší než nezpracovaný ukazatel (typicky 2 ukazatele: jeden na objekt, jeden na řídicí blok) a počítání referencí má atomovou režii při každé kopii/zničení. Používejte jej pouze v případě, že je vlastnictví skutečně sdíleno.

make_shared a Reference Counting

std::make_shared(args...)přiděluje řídicí blok objektu AND v jednom přidělení (místo dvou). To je šetrnější k mezipaměti a vyhnete se problému s bezpečností výjimek u surovéhonew.

shared_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Logger {
    std::string name;
    Logger(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }
    ~Logger() { std::cout << name << " destroyed\n"; }
    void log(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << '\n';
    }
};

int main() {
    auto logger = std::make_shared<Logger>("AppLogger");
    std::cout << "use_count: " << logger.use_count() << '\n';  // 1

    {
        auto copy = logger;  // shared ownership
        std::cout << "use_count: " << logger.use_count() << '\n';  // 2
        copy->log("inside scope");
    }  // copy destroyed, count drops to 1

    std::cout << "use_count: " << logger.use_count() << '\n';  // 1
    logger->log("still alive");
}  // logger destroyed — count drops to 0, Logger deleted

slabé_ptr: Přerušovací cykly

std::weak_ptrpozoruje objekt spravovanýshared_ptrbez prodloužení jeho životnosti. Pro přístup k objektu zavolejtelock(), který vrátíshared_ptr— nebonullptr, pokud byl objekt již zničen.weak_ptrje nezbytný pro prolomení kruhových referencí, které by jinak unikaly.

weak_ptr_example.cpp
#include <iostream>
#include <memory>
#include <string>

struct Employee;
struct Team {
    std::string name;
    std::vector<std::shared_ptr<Employee>> members;
    Team(std::string n) : name(std::move(n)) {}
    ~Team() { std::cout << "Team " << name << " destroyed\n"; }
};

struct Employee {
    std::string name;
    std::weak_ptr<Team> team;  // weak — does NOT own the team
    Employee(std::string n) : name(std::move(n)) {}
    ~Employee() { std::cout << "Employee " << name << " destroyed\n"; }

    void print_team() {
        if (auto t = team.lock()) {  // try to get shared_ptr
            std::cout << name << " belongs to " << t->name << '\n';
        } else {
            std::cout << name << " has no team\n";
        }
    }
};

int main() {
    auto team = std::make_shared<Team>("Engineering");
    auto alice = std::make_shared<Employee>("Alice");
    auto bob = std::make_shared<Employee>("Bob");

    team->members.push_back(alice);
    team->members.push_back(bob);
    alice->team = team;  // weak_ptr — no circular ownership
    bob->team = team;

    alice->print_team();  // "Alice belongs to Engineering"

    team.reset();  // destroy team
    alice->print_team();  // "Alice has no team" — weak_ptr expired
}
Úskalí

Pokud dva objekty držíshared_ptru sebe, žádný z nich nikdy nemůže dosáhnout referenčního počtu nula – udrží se navzájem naživu navždy. Toto je únik paměti, i když používáte chytré ukazatele.

Ukazatel ``cpp
struct Node {
std::shared_ptr next; // if next points back, LEAK!
};
auto a = std::make_shared();
auto b = std::make_shared();
a->next = b;
b->next = a; // circular — neither a nor b will ever be destroyed
``

The fix: make one direction a weak_ptr. In tree structures, children hold weak_ptr to parents (or raw observer pointers). In doubly-linked lists, use weak_ptr for the předchozí.

Aliasing Constructor & enable_shared_from_this

Konstruktor aliasingu vytvoříshared_ptr, který sdílí vlastnictví s existujícímshared_ptr, ale ukazuje na jiný objekt (obvykle člen). Objekt s aliasem zůstává aktivní, dokud existuje jakýkolishared_ptrs aliasem:

struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(Pair{1, 2});
std::shared_ptr<int> sp_first(p, &p->first);  // shares p's control block
// p can go out of scope — Pair stays alive because sp_first still references it

enable_shared_from_this is a CRTP base class that allows an object managed by shared_ptr to obtain a shared_ptr to itself via shared_from_this(). Without it, constructing a new shared_ptr from this vytváří druhý řídicí blok, což vede k dvojitému uvolnění.

Osvědčený postup

shared_ptrse nadměrně používá. Sáhněte po něm pouze tehdy, když je vlastnictví skutečně sdíleno napříč nezávislými životnostmi. NEPOUŽÍVEJTEshared_ptrjako línou náhradu za přemýšlení o vlastnictví.

Vyhněte seshared_ptr, když:
- Existuje jeden vlastník – místo toho použijteunique_ptr(nulová režie).
- Objekt žije na hromádce – stačí použít objekt přímo.
- Stačí předat referenci — předatT&neboT*.
- Používáte jej „protože je to bezpečné“ —shared_ptrmá provozní náklady (přepočet atomů, další přidělení) a skrývá sémantiku vlastnictví.
- V drahách kritických pro výkon — operace atomového přepočtu jsou překvapivě drahé.

Legitimní scénáře sdíleného vlastnictví: mezipaměti (vícenásobné vyhledávání vrací stejný objekt), systémy pub/sub, sdílené konfigurační objekty a datové struktury grafů se skutečně sdílenými uzly.

Vnitřní části řídicího bloku

Pochopení řídicího bloku pomáhá vysvětlit nákladyshared_ptr.make_sharedkombinuje objekt a řídicí blok do jedné alokace. Konstrukce ze surovéhonewvyžaduje dvě samostatné alokace.

control_block.cpp
#include <memory>
#include <iostream>

struct Widget {
    int data[4];
};

int main() {
    // Two allocations: one for Widget, one for control block
    std::shared_ptr<Widget> p1(new Widget{});

    // Single allocation: Widget + control block together
    auto p2 = std::make_shared<Widget>();

    // Sizes (typical 64-bit platform)
    std::cout << "sizeof(Widget*):          " << sizeof(Widget*) << '\n';   // 8
    std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n';  // 8
    std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n';  // 16

    // shared_ptr is 2 pointers: one to object, one to control block
    // Control block contains: strong count, weak count, deleter, allocator

    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // 1
    std::weak_ptr<Widget> w = p2;
    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // still 1
    // weak_ptr does NOT increase strong count
}
Klíčové poznatky
  • shared_ptrpoužívá počítání referencí — objekt zemře, když je poslední vlastník pryč
  • make_sharedalokuje objekt + řídicí blok v jedné alokaci (preferujte to vždy)
  • weak_ptrpozoruje bez vlastnictví – použijtelock()k bezpečnému přístupu, přeruší cyklické odkazy
  • shared_ptrje široký 2 ukazatele a má režii atomového přepočtu – není zdarma
  • Výchozíunique_ptr;shared_ptrpoužívejte pouze v případě, že je vlastnictví skutečně sdíleno
  • Nikdy nevytvářejte dvashared_ptrze stejného nezpracovaného ukazatele – použijteenable_shared_from_thispro vlastní odkazy

Kvíz — Otestujte své znalosti

(15 XP)

1. Co se stane, když dva objekty drží`shared_ptr`u sebe?

2. Proč je`std::make_shared`preferován před`shared_ptr<T>(new T(...))`?

3. Jak bezpečně přistupujete k objektu prostřednictvím`weak_ptr`?