22.5 — std::unique_ptr

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 destroyed

Ponieważ 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 scope

Wypisuje:

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 here

Powyż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;
}

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
261 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze