Jste zde

Typické chyby při používání chytrých ukazatelů v C++

V příspěvku bych chtěl poukázat na některé typické chyby, kterých se programátoři dopouští, když začínají v C++ pracovat s chytrými ukazateli.

Úvod

Od C++11 jsou ve standardní knihovně k dispozici tzv. chytré ukazatele. Jejich hlavním cílem je poskytnout prostředek pro bezpečnou správu paměti, jež usnadňuje vyhnutí se mnoha programátorským chybám. Mezi tyto chyby patří např. neuvolnění paměti (paměťové úniky), vícenásobné uvolnění paměti, ukazatele ukazující do neplatné paměti, použití delete místo delete[] a další. Problémem tradičních ukazatelů je, že není zřejmé, kdo je jejich vlastníkem (a měl by je tedy uvolnit) a kdo všechno je používá (kdy je uvolnit?).

Bohužel, použití chytrých ukazatelů není všelék. Je potřeba je použít korektně. Jinak si člověk může místo vyřešení problémů přidělat problémy další. Ve zbytku příspěvku bych chtěl proto poukázat na některé typické chyby, kterých se programátoři začínající s chytrými ukazateli dopouští.

Pojďme tedy na to.

Vytváření chytrých ukazatelů z tradičních ukazatelů až se zpožděním

Chytré ukazatele std::shared_ptr a std::unique_ptr při konstrukci z tradičního ukazatele (angl. raw pointer) za něj přebírají odpovědnost. Tedy se automaticky postarají o jeho destrukci. Pro je velmi žádoucí jejich konstrukci z takového ukazatele zbytečně neoddalovat. Jinak může snadno dojít k žádnému či několikanásobnému uvolnění paměti. Vezměme si následující příklad:

int *i = new int(1);
// ...
std::shared_ptr<int> p1(i);
// ...
std::shared_ptr<int> p2(i);

Na konci takového bloku dojde k dvojnásobnému uvolnění paměti ukazatele i, protože každý z chytrých ukazatelů p1 a p2 za onu paměť převezme odpovědnost. Každý z std::shared_ptr totiž počítá s tím, že předaný (ne-chytrý) ukazatel nikdo jiný nevlastní. Další problém nastane, pokud mezi zavoláním new a vytvořením chytrého ukazatele dojde k vyhození výjimky. Alokovaná paměť pak nebude uvolněna a paměťový únik je na světě. Z toho důvodu, pokud už vytváříte chytrý ukazatel z tradičního ukazatele alokovaného pomocí new, udělejte to hned. Např. následující kód je již korektní:

std::shared_ptr<int> p1(new int(1));
// ...
std::shared_ptr<int> p2(p1);

V ideálním případě byste měli použít new přímo při vytváření chytrého ukazatele (nebo použít alternativy, viz dále).

Vytváření chytrých ukazatelů přes new u konstrukcí s nejistým pořadím vyhodnocení

Mějme následující volání funkce func se dvěma argumenty:

func(std::shared_ptr<MyClass>(new MyClass), getSomething());

Problém nastane, pokud getSomething() může vyhodit výjimku. Pořadí vyhodnocení argumentů funkcí je závislé na překladači a klidně by to mohlo dopadnout takto:

  1. MyClass *tmp = new MyClass
  2. getSomething()
  3. std::shared_ptr<MyClass>(tmp)

Jak již určitě vidíte, pokud getSomething() vyhodí výjimku, tak dojde k úniku paměti. Řešením je použít buď std::make_shared() (či std::make_unique() pro std::unique_ptr):

func(std::make_shared<MyClass>(), getSomething());

nebo si ukazatel vytvořit před zavoláním oné funkce:

std::shared_ptr<MyClass> p(new MyClass);
func(p, getSomething());

Pojďme se nyní na ony vytvářecí funkce podívat detailněji.

Ignorování std::make_shared() a std::make_unique()

Funkce std::make_shared() a od C++14 std::make_unique() představují alternativní cestu vytváření chytrých ukazatelů. Pomocí nich místo

std::shared_ptr<MyClass> p(new MyClass(1, 2));

nebo

auto p = std::shared_ptr<MyClass>(new MyClass(1, 2));

můžete psát jen

auto p = std::make_shared<MyClass>(1, 2);

Stejně tak u std::unique_ptr a std::make_unique(). Použití těchto funkcí má následující výhody:

  1. Stručnější zápis. Není potřeba opakovat název typu.
  2. Vyhnutí se problémům s pořadím vyhodnocování výrazů. Viz dříve uvedený příklad.
  3. Efektivita. Použití std::make_shared() je efektivnější, než použití new a následné vytvoření std::shared_ptr. Je to dáno tím, jak je std::shared_ptr implementován. Ve stručnosti: při použití new a následné konstrukci std::shared_ptr dojde vlastně k dvojnásobné alokaci paměti (jednou pro vytvořený objekt, jednou pro kontrolní blok, kterým si std::shared_ptr hlídá počet referencí). Při použití std::make_shared() dojde jen k jediné alokaci (paměť pro objekt a kontrolní blok se alokuje naráz).

Proto preferujte std::make_shared() a std::make_unique() k vytváření chytrých ukazatelů.

Vracení std::shared_ptr místo std::unique_ptr, když to není nutné

Pokud máte funkci, která vytváří dynamicky alokovaný objekt a vrací jej v chytrém ukazateli s tím, že si jej interně nikde neuchovává, tak byste jej měli vrátit v std::unique_ptr:

std::unique_ptr<MyClass> MyClass::create() {
    // ...
    return std::unique_ptr<MyClass>(new MyClass); // Nebo od C++14 return std::make_unique<MyClass>();
}

Pokud byste v tomto případě vraceli std::shared_ptr

std::shared_ptr<MyClass> create() {
    // ...
    return std::shared_ptr<MyClass>(new MyClass); // Nebo return std::make_shared<MyClass>();
}

tak zbytečně limitujete volajícího. Z výsledného std::unique_ptr lze udělat std::shared_ptr, ale naopak to již nejde. Příčinou je, že nepoužíváte korektní typ chytrého ukazatele. Skutečně, tím, že vracíte std::shared_ptr říkáte, že onen ukazatel používá ještě někdo jiný. To v tomto případě ale není pravda.

Použití metody get() pro přístup k objektu přes ukazatel

Když přistupujete k dynamicky alokovanému objektu přes chytrý ukazatel, tak není potřeba volat get(). Stačí přímý přístup. Pokud máme např. následující ukazatel

std::unique_ptr<MyClass> p(new MyClass);

tak místo

p.get()->foo();

stačí psát

p->foo();

Použití get() je v těchto případech zbytečné.

Vytváření chytrých ukazatelů přímo z this

Představte si, že se nacházíte v metodě třídy MyClass a potřebujete zavolat následující funkci:

void func(std::shared_ptr<MyClass> p);

Zavolání této funkce s this je typicky cesta do země nedefinovaného chování:

void MyClass:foo() {
    // ...
    func(this);
}

Problém je, že typem this je tradiční ukazatel MyClass *, a std::shared_ptr za něj převezme při vytváření odpovědnost. Tudíž může dojít k uvolnění paměti, což je něco podobného, jako byste napsali toto:

void MyClass::foo() {
    // ...
    delete this; // Sebevražda?
}

Co s tím? Řešení je mírně komplikovanější. MyClass musí dědit od std::enable_shared_from_this a při zavolání použijete k získání chytrého ukazatele std::shared_from_this():

class MyClass: public std::enable_shared_from_this {
    // ...
};
 
void MyClass::foo() {
    // ...
    func(std::shared_from_this());
}

Vytváření cyklických závislostí bez použití std::weak_ptr

std::shared_ptr bývá implementován technikou počítání referencí. Problémem u této techniky bývá uvolňování paměti v případě, kdy se vyskytnou cykly:

// Uzel ve stromu.
class Node {
    // ...
private:
    std::vector<std::shared_ptr<Node>> children;
    std::shared_ptr<Node> parent;
};

Pokud má uzel ve stromu ukazatel na své potomky a každý potomek má ukazatel na otce, tak to vede na cyklus. Tudíž nikdy nemusí dojít k uvolnění paměti. Je si potřeba uvědomit, že pouhé použití chytrých ukazatelů za vás všechny problémy nevyřeší.

K vyřešení tohoto problému slouží třetí typ chytrých ukazatelů ze standardní knihovny: std::weak_ptr. Pokud by tedy ve vašem kódu mělo dojít k vytvoření cyklu, tak použijte právě std::weak_ptr.

Pamatujte, nestačí používat jediný typ chytrých ukazatelů. Každý z nich má jiné použití a je potřeba je vhodně kombinovat.

Další čtení

Určitě doporučuji si přečíst kapitolu 4 z Effective Modern C++, kde Scott Meyers ukazuje, jak chytré ukazatele efektivně využívat.

Přidat komentář