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