1 września 2013

[C++11] Dynamiczna pamięć (new + inteligentne wskaźniki)

Nowy standard, dostarczy kilka mechanizmów pozwalających lepiej radzić sobie z zarządzaniem obiektami w pamięci dynamicznej. Prócz nowości, opisałem kilka już istniejących mechanizmów, które zawsze wylatują mi z głowy.

new


Operator new, umożliwia nie tylko alokowanie pamięci pod nowo tworzone obiekty, ale również pod wskazany przez nas (wcześniej zaalokowany) obszar pamięci.
#include <iostream>

int main() {
    int int_in_memory = 5;
    std::cout << "Memory (val)  " << int_in_memory << std::endl;
    std::cout << "Memory (addr) " << &int_in_memory << std::endl;

    int *ptr = new (&int_in_memory) int(2);
    std::cout << "Ptr    (val)  " << *ptr << std::endl;
    std::cout << "Ptr    (addr) " << ptr << std::endl;

    return 0;
}
W takim przypadku, new zawsze zwróci ten sam adres, który został mu przekazany. Nie można łączyć tego rodzaju inicjalizacji z nothrow (wersja która nie rzuca wyjątku)
Memory (val)  5
Memory (addr) 0xbf9c26f8
Ptr    (val)  2
Ptr    (addr) 0xbf9c26f8
Każda pamięć jawnie zaalokowana przez nas musi być później zwolniona. W przypadku tablic trzeba użyć specjalnej formy delete [], aby wywołać destruktory obiektów, które się w niej znajdują.
#include <iostream>

struct MyObject {
    MyObject(std::string n): name(n) { std::cout << "Constructor: " << name << std::endl; }
    ~MyObject() noexcept { std::cout << "Destructor:  " << name << std::endl; }
    std::string name;
};

int main() {
    MyObject *tab = new MyObject[3] { {"Elem1"},
                                      {"Elem2"},
                                      {"Elem3"} };

    delete tab;
//  delete [] tab;

    return 0;
}
Jeżeli skorzystamy ze zwykłego delete, bez podania pustych nawiasów, zachowanie jest niezdefiniowane.
Constructor: Elem1
Constructor: Elem2
Constructor: Elem3
Destructor:  Elem1
The program has unexpectedly finished.
Nowy standard dodaje, wersje operatora new z nothrow i w razie wystąpienia błędu przy inicjalizacji pamięci zamiast wyjątku operator zwróci nullptr, aby poinformować nas o problemie.
#include <iostream>
#include <memory>

int main() {
    try {
        int *ptr = new int[1000000000];
    }
    catch (std::bad_alloc& exception) {
        std::cout << "Exception: " << exception.what() << std::endl;
    }

    int *ptr = new (std::nothrow) int[1000000000];
    if (ptr == nullptr)
        std::cout << "Not allocated" << std::endl;

    return 0;
}
Poniżej wyniki działania programu, w pierwszej wersji zwracany jest wyjątek, w drugiej jesteśmy informowani przez zwrócenie nullptr.
Exception: std::bad_alloc
Not allocated
Co ciekawe, rzadko można w kodzie zauważyć sytuacje, gdy programista próbuje przestrzec się przed tego typu zdarzeniami (blok try-catch). W Linuxie jest to związane z domyślnie włączonym mechanizmem "opportunistic memory allocation", który nie sprawdza, czy pamięć jest dostępna gdy próbujemy ją zaalokować. W momencie, gdy próbujemy się do niej odwołać, a w systemie jej zabraknie program zostaje zterminowany. Ustawienie można sprawdzić przez:
cat /proc/sys/vm/overcommit_memory
0

smart pointers


W standardzie pojawiły się nowe inteligentne wskaźniki, zaczerpnięte z biblioteki boost. Są to unique_ptr (zdaje się odpowiednik boost::scoped_ptr), shared_ptr oraz weak_ptr. Herb Shutter daje kilka porad, odnośnie tego jaki i kiedy je stosować.
Można to podsumować w ten sposób. W nowoczesnym C++ zawsze powinno się używać inteligentnych wskaźników oraz surowych wskaźników nie posiadających prawa własności. unique_ptr powinno być preferowane nad shared_ptr (zawsze można będzie na niego przejść, jeżeli nasz zasób będzie musiał być współdzielony między komponentami o różnym czasie życia, albo gdy chcemy skorzystać z własnej metody do usunięcia zasobu z pamięci). Poniżej program, w którym m.in. shared_ptr, korzysta z funkcji "usuwającej" zasób z pamięci dostarczonej przez nas.
#include <iostream>
#include <memory>

struct MyObject {
    MyObject(std::string n): name(n) { std::cout << "Constructor: " << name << std::endl; }
    ~MyObject() noexcept { std::cout << "Destructor:  " << name << std::endl; }
    std::string name;
};

void myDeleter(MyObject* p) {
    std::cout << "Deleter:     " << p->name << std::endl;
}

int main() {
    std::shared_ptr<MyObject> sha(new MyObject("shared"), myDeleter);

    std::unique_ptr<MyObject> uni1(new MyObject("unique"));
    std::unique_ptr<MyObject> uni2(uni1.release());

    std::cout << "Uni1 -> " << (uni1 ? "exist" : "not exist") << std::endl;
    std::cout << "Uni2 -> " << (uni2 ? "exist" : "not exist") << std::endl;

    auto store = std::make_shared<MyObject>("weak");
    std::weak_ptr<MyObject> wea(store);

    if (std::shared_ptr<MyObject> sp = wea.lock())
        std::cout << "Locked:      " << sp->name << std::endl;

    return 0;
}
A oto kolejność wywołania destruktorów dla dynamicznie stworzonych zasobów.
Constructor: shared
Constructor: unique
Uni1 -> not exist
Uni2 -> exist
Constructor: weak
Locked:      weak
Destructor:  weak
Destructor:  unique
Deleter:     shared
Shutter (i nie tylko) zaleca tworzenie inteligentnych wskaźników za pomocą metod make_*. Ale to postanowiłem przetestować sobie w innym wpisie.

std::allocator


Ostatnią rzeczą, którą testowałem są alokatory. Pozwalają nam one na odseparowanie procesu alokowania od konstruowania. W przykładzie poniżej, szykujemy pamięć pod trzy elementy typu MyObject. Najpierw pamięć jest alokowana, a my uzyskujemy wskaźnik pod adres, gdzie powinien być skonstruowany pierwszy z naszych obiektów. Następnie korzystają z metody construct(), oraz przesuwając wskaźnik następuje proces konstruowania. Aby zniszczyć obiekt, należy skorzystać z metody destroy().
#include <iostream>
#include <memory>


struct MyObject {
    MyObject(std::string n): name(n) { std::cout << "Constructor: " << name << std::endl; }
    ~MyObject() noexcept { std::cout << "Destructor:  " << name << std::endl; }
    std::string name;
};

int main() {
    std::allocator<MyObject> alloc;
    std::cout << "Allocate" << std::endl;
    MyObject *ptr = alloc.allocate(3);

    std::cout << "Address:     " << ptr << std::endl;
    alloc.construct(ptr, "Elem1");

    ptr++;
    std::cout << "Address:     " << ptr << std::endl;
    alloc.construct(ptr, "Elem2");

    return 0;
}
Wynik działania.
Allocate
Address:     0x8b2a008
Constructor: Elem1
Address:     0x8b2a00c
Constructor: Elem2

Brak komentarzy:

Prześlij komentarz