Na początku rozdziału omówiliśmy, w jaki sposób użycie wskaźników może w niektórych sytuacjach prowadzić do błędów i wycieków pamięci. Może się to na przykład zdarzyć, gdy funkcja wcześniej zwróci lub zgłosi wyjątek, a wskaźnik nie zostanie poprawnie usunięty.
#include <iostream>
void someFunction()
{
auto* ptr{ new Resource() };
int x{};
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
throw 0; // the function returns early, and ptr won’t be deleted!
// do stuff with ptr here
delete ptr;
}Teraz, gdy omówiliśmy podstawy semantyki przenoszenia, możemy wrócić do tematu klas inteligentnych wskaźników. Chociaż inteligentne wskaźniki mogą oferować inne funkcje, cechą charakterystyczną inteligentnego wskaźnika jest to, że zarządza on dynamicznie przydzielanymi zasobami dostarczonymi przez użytkownika inteligentnego wskaźnika i zapewnia prawidłowe czyszczenie dynamicznie przydzielonego obiektu w odpowiednim czasie (zwykle gdy inteligentny wskaźnik wykracza poza zakres).
Z tego powodu inteligentne wskaźniki nigdy nie powinny być same dynamicznie alokowane (w przeciwnym razie istnieje ryzyko, że inteligentny wskaźnik nie zostanie prawidłowo zwolniony, co oznacza, że jest to obiekt, którego jest właścicielem). nie zostanie zwolniony, co spowoduje wyciek pamięci). Zawsze przydzielając inteligentne wskaźniki na stosie (jako zmienne lokalne lub elementy kompozycji klasy), mamy gwarancję, że inteligentny wskaźnik prawidłowo wyjdzie poza zakres, gdy funkcja lub obiekt, w którym się zawiera, zakończy się, zapewniając prawidłowe zwolnienie obiektu, którego właścicielem jest inteligentny wskaźnik.
Standardowa biblioteka C++11 jest dostarczana z 4 klasami inteligentnych wskaźników: std::auto_ptr (usunięte w C++17), std::unique_ptr, std::shared_ptr i std::weak_ptr. std::unique_ptr jest zdecydowanie najczęściej używaną klasą inteligentnych wskaźników, więc omówimy ją w pierwszej kolejności. W kolejnych lekcjach omówimy std::shared_ptr i std::weak_ptr.
std::unique_ptr
std::unique_ptr to zamiennik std::auto_ptr w C++ 11. Należy go używać do zarządzania dowolnym dynamicznie przydzielanym obiektem, który nie jest współdzielony przez wiele obiektów. Oznacza to, że std::unique_ptr powinien całkowicie posiadać obiekt, którym zarządza, a nie dzielić się tą własnością z innymi klasami. std::unique_ptr znajduje się w nagłówku <memory>.
Przyjrzyjmy się prostemu przykładowi inteligentnego wskaźnika:
#include <iostream>
#include <memory> // for std::unique_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// allocate a Resource object and have it owned by std::unique_ptr
std::unique_ptr<Resource> res{ new Resource() };
return 0;
} // res goes out of scope here, and the allocated Resource is destroyedPonieważ std::unique_ptr jest tutaj przydzielony na stosie, z pewnością ostatecznie wyjdzie poza zakres, a kiedy to nastąpi, usunie zasób, którym zarządza.
W przeciwieństwie do std::auto_ptr, std::unique_ptr poprawnie implementuje semantykę przenoszenia.
#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
std::unique_ptr<Resource> res1{ new Resource{} }; // Resource created here
std::unique_ptr<Resource> res2{}; // Start as nullptr
std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");
// res2 = res1; // Won't compile: copy assignment is disabled
res2 = std::move(res1); // res2 assumes ownership, res1 is set to null
std::cout << "Ownership transferred\n";
std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");
return 0;
} // Resource destroyed here when res2 goes out of scopeWypisuje:
Resource acquired res1 is not null res2 is null Ownership transferred res1 is null res2 is not null Resource destroyed
Ponieważ std::unique_ptr został zaprojektowany z myślą o semantyce przenoszenia, inicjowanie kopiowania i przypisywanie kopii są wyłączone. Jeśli chcesz przenieść zawartość zarządzaną przez std::unique_ptr, musisz użyć semantyki przenoszenia. W powyższym programie osiągamy to poprzez std::move (który konwertuje res1 na wartość r, co wyzwala przypisanie przeniesienia zamiast przypisania kopii).
Dostęp do zarządzanego obiektu
std::unique_ptr ma przeciążone operatory* i operator->, których można użyć do zwrócenia zarządzanego zasobu. Operator* zwraca referencję do zarządzanego zasobu, a operator-> zwraca wskaźnik.
Pamiętaj, że std::unique_ptr nie zawsze zarządza obiektem - albo dlatego, że został utworzony pusty (przy użyciu domyślnego konstruktora lub przekazując nullptr jako parametr), albo dlatego, że zarządzany zasób został przeniesiony do innego std::unique_ptr. Zanim więc użyjemy któregokolwiek z tych operatorów, powinniśmy sprawdzić, czy std::unique_ptr rzeczywiście ma zasób. Na szczęście jest to proste: std::unique_ptr rzutuje na wartość bool, która zwraca wartość true, jeśli std::unique_ptr zarządza zasobem.
Oto przykład:
#include <iostream>
#include <memory> // for std::unique_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
std::ostream& operator<<(std::ostream& out, const Resource&)
{
out << "I am a resource";
return out;
}
int main()
{
std::unique_ptr<Resource> res{ new Resource{} };
if (res) // use implicit cast to bool to ensure res contains a Resource
std::cout << *res << '\n'; // print the Resource that res is owning
return 0;
}Wypisuje:
Resource acquired I am a resource Resource destroyed
W powyższym programie używamy przeciążonego operatora*, aby uzyskać obiekt Resource będący własnością std::unique_ptr res, który następnie wysyłamy do std::cout drukowanie.
std::unique_ptr i tablice
W przeciwieństwie do std::auto_ptr, std::unique_ptr jest wystarczająco inteligentny, aby wiedzieć, czy użyć usuwania skalarnego, czy usunięcia tablicy, więc std::unique_ptr można używać zarówno z obiektami skalarnymi, jak i tablicami.
Jednak std::array lub std::vector (lub std::string) są prawie zawsze lepszym wyborem niż użycie std::unique_ptr z stała tablica, tablica dynamiczna lub ciąg w stylu C.
Najlepsza praktyka
Preferuj std::array, std::vector lub std::string nad inteligentnym wskaźnikiem zarządzającym stałą tablicą, tablicą dynamiczną lub ciągiem w stylu C.
std::make_unique
C++ 14 jest wyposażony w dodatkową funkcję o nazwie std::make_unique(). Ta funkcja oparta na szablonie konstruuje obiekt typu szablon i inicjuje go argumentami przekazanymi do funkcji.
#include <memory> // for std::unique_ptr and std::make_unique
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
Fraction(int numerator = 0, int denominator = 1) :
m_numerator{ numerator }, m_denominator{ denominator }
{
}
friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
};
int main()
{
// Create a single dynamically allocated Fraction with numerator 3 and denominator 5
// We can also use automatic type deduction to good effect here
auto f1{ std::make_unique<Fraction>(3, 5) };
std::cout << *f1 << '\n';
// Create a dynamically allocated array of Fractions of length 4
auto f2{ std::make_unique<Fraction[]>(4) };
std::cout << f2[0] << '\n';
return 0;
}Powyższy kod wyświetla:
3/5 0/1
Użycie std::make_unique() jest opcjonalne, ale jest zalecane zamiast samodzielnego tworzenia std::unique_ptr. Dzieje się tak dlatego, że kod używający std::make_unique jest prostszy, a także wymaga mniej pisania (w przypadku użycia z automatycznym odliczaniem typu). Co więcej, w C++14 rozwiązano problem związany z bezpieczeństwem wyjątków, który może wynikać z pozostawienia przez C++ kolejności oceniania argumentów funkcji nieokreślonej.
Najlepsza praktyka
Użyj std::make_unique() zamiast tworzyć std::unique_ptr i samodzielnie używać new.
Bardziej szczegółowy problem bezpieczeństwa wyjątków
Dla tych, którzy zastanawiają się, czym jest wspomniany powyżej „problem bezpieczeństwa wyjątku”, oto opis problem.
Rozważ wyrażenie takie jak to:
some_function(std::unique_ptr<T>(new T), function_that_can_throw_exception());Kompilator ma dużą elastyczność w zakresie obsługi tego wywołania. Może utworzyć nowy T, następnie wywołać funkcję Function_that_can_throw_exception(), a następnie utworzyć std::unique_ptr, który zarządza dynamicznie przydzielonym T. Jeśli funkcjafunction_that_can_throw_exception() zgłosi wyjątek, przydzielony T nie zostanie cofnięty, ponieważ inteligentny wskaźnik do przeprowadzenia dezalokacji nie został jeszcze utworzony. Prowadzi to do wycieku T.
std::make_unique() nie jest dotknięty tym problemem, ponieważ utworzenie obiektu T i utworzenie std::unique_ptr odbywa się wewnątrz funkcji std::make_unique(), gdzie nie ma dwuznaczności co do kolejności wykonywania.
Ten problem został rozwiązany w C++17, ponieważ nie można już oceniać argumentów funkcji przeplatane.
Zwracanie std::unique_ptr z funkcji
std::unique_ptr można bezpiecznie zwrócić z funkcji według wartości:
#include <memory> // for std::unique_ptr
std::unique_ptr<Resource> createResource()
{
return std::make_unique<Resource>();
}
int main()
{
auto ptr{ createResource() };
// do whatever
return 0;
}W powyższym kodzie createResource() zwraca std::unique_ptr według wartości. Jeśli ta wartość nie jest do niczego przypisana, tymczasowa wartość zwracana wyjdzie poza zakres i zasób zostanie wyczyszczony. Jeśli jest przypisany (jak pokazano w funkcji main()), w języku C++ 14 lub starszym zostanie zastosowana semantyka przenoszenia w celu przeniesienia zasobu z wartości zwracanej do przypisanego obiektu (w powyższym przykładzie ptr), a w języku C++ 17 lub nowszym zwrot zostanie pominięty. To sprawia, że zwracanie zasobu przez std::unique_ptr jest znacznie bezpieczniejsze niż zwracanie surowych wskaźników!
Ogólnie rzecz biorąc, nie powinieneś zwracać std::unique_ptr przez wskaźnik (nigdy) lub referencję (chyba że masz ku temu konkretny powód).
Przekazywanie std::unique_ptr do funkcji
Jeśli chcesz, aby funkcja przejęła na własność zawartość wskaźnika, przekaż std::unique_ptr według wartości. Pamiętaj, że ponieważ semantyka kopiowania została wyłączona, będziesz musiał użyć std::move, aby faktycznie przekazać zmienną.
#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
std::ostream& operator<<(std::ostream& out, const Resource&)
{
out << "I am a resource";
return out;
}
// This function takes ownership of the Resource, which isn't what we want
void takeOwnership(std::unique_ptr<Resource> res)
{
if (res)
std::cout << *res << '\n';
} // the Resource is destroyed here
int main()
{
auto ptr{ std::make_unique<Resource>() };
// takeOwnership(ptr); // This doesn't work, need to use move semantics
takeOwnership(std::move(ptr)); // ok: use move semantics
std::cout << "Ending program\n";
return 0;
}Powyższy program wypisuje:
Resource acquired I am a resource Resource destroyed Ending program
Pamiętaj, że w tym przypadku własność zasobu została przeniesiona do takeOwnership(), więc zasób został zniszczony na końcu takeOwnership(), a nie na końcu main().
Jednak w większości przypadków nie chcesz, aby funkcja przejęła na własność zasób.
Chociaż możesz przekazać std::unique_ptr przez stałą referencję (co pozwoli funkcji używać obiektu bez przejmowania własności), lepiej jest po prostu przekazać sam zasób (przez wskaźnik lub referencję, w zależności od tego, czy null jest poprawnym argumentem). Dzięki temu funkcja pozostaje niezależna od sposobu, w jaki wywołujący zarządza swoimi zasobami.
Aby uzyskać surowy wskaźnik z std::unique_ptr, możesz użyć funkcji członkowskiej get():
#include <memory> // for std::unique_ptr
#include <iostream>
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
std::ostream& operator<<(std::ostream& out, const Resource&)
{
out << "I am a resource";
return out;
}
// The function only uses the resource, so we'll accept a pointer to the resource, not a reference to the whole std::unique_ptr<Resource>
void useResource(const Resource* res)
{
if (res)
std::cout << *res << '\n';
else
std::cout << "No resource\n";
}
int main()
{
auto ptr{ std::make_unique<Resource>() };
useResource(ptr.get()); // note: get() used here to get a pointer to the Resource
std::cout << "Ending program\n";
return 0;
} // The Resource is destroyed herePowyższy program wypisuje:
Resource acquired I am a resource Ending program Resource destroyed
std::unique_ptr i klas
Możesz oczywiście użyć std::unique_ptr jako elementu składowego swojej klasy. W ten sposób nie musisz martwić się o to, że destruktor klasy usunie pamięć dynamiczną, ponieważ std::unique_ptr zostanie automatycznie zniszczony, gdy obiekt klasy zostanie zniszczony.
Jeśli jednak obiekt klasy nie zostanie poprawnie zniszczony (np. zostanie dynamicznie przydzielony i nie zostanie poprawnie cofnięty), wówczas element std::unique_ptr również nie zostanie zniszczony, a obiekt zarządzany przez std::unique_ptr nie zostanie zniszczony zostać zwolniony.
Niewłaściwe użycie std::unique_ptr
Istnieją dwa proste sposoby niewłaściwego użycia std::unique_ptrs i obu można łatwo uniknąć. Po pierwsze, nie pozwól, aby wiele obiektów zarządzało tym samym zasobem. Na przykład:
Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
std::unique_ptr<Resource> res2{ res };Chociaż jest to zgodne ze składnią, efektem końcowym będzie próba usunięcia zasobu zarówno res1, jak i res2, co doprowadzi do niezdefiniowanego zachowania.
Po drugie, nie usuwaj ręcznie zasobu spod std::unique_ptr.
Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;Jeśli to zrobisz, std::unique_ptr spróbuje usunąć już usunięty zasób, ponownie prowadząc na niezdefiniowane zachowanie.
Zauważ, że std::make_unique() zapobiega przypadkowemu wystąpieniu obu powyższych przypadków.
Czas quizu
Pytanie nr 1
Przekształć następujący program z używania normalnego wskaźnika na używanie std::unique_ptr tam, gdzie to konieczne:
#include <iostream>
class Fraction
{
private:
int m_numerator{ 0 };
int m_denominator{ 1 };
public:
Fraction(int numerator = 0, int denominator = 1) :
m_numerator{ numerator }, m_denominator{ denominator }
{
}
friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
};
void printFraction(const Fraction* ptr)
{
if (ptr)
std::cout << *ptr << '\n';
else
std::cout << "No fraction\n";
}
int main()
{
auto* ptr{ new Fraction{ 3, 5 } };
printFraction(ptr);
delete ptr;
return 0;
}
