std::unique_ptr
obsługuje tablice, na przykład:
std::unique_ptr<int[]> p(new int[10]);
ale czy jest to potrzebne? prawdopodobnie jest wygodniejszy w użyciu std::vector
lub std::array
.
Czy znajdujesz zastosowanie dla tego konstruktu?
źródło
std::unique_ptr
obsługuje tablice, na przykład:
std::unique_ptr<int[]> p(new int[10]);
ale czy jest to potrzebne? prawdopodobnie jest wygodniejszy w użyciu std::vector
lub std::array
.
Czy znajdujesz zastosowanie dla tego konstruktu?
Niektóre osoby nie mają luksusu korzystania std::vector
, nawet z przydzielającymi. Niektóre osoby potrzebują dynamicznej tablicy, więc nie std::array
ma. Niektóre osoby pobierają tablice z innego kodu, o którym wiadomo, że zwraca tablicę; i ten kod nie zostanie przepisany w celu zwrócenia vector
czegoś lub czegoś.
Pozwalając unique_ptr<T[]>
, zaspokajasz te potrzeby.
Krótko mówiąc, używasz, unique_ptr<T[]>
kiedy potrzebujesz . Kiedy alternatywy po prostu nie będą dla ciebie działać. To narzędzie ostateczności.
vector
”. Możesz spierać się, czy są to rozsądne wymagania, czy nie, ale nie możesz zaprzeczyć, że one istnieją .
std::vector
gdyby mógł std::unique_ptr
.
unique_ptr
nich, ale takie projekty naprawdę istnieją.
Są kompromisy i wybierasz rozwiązanie, które pasuje do tego, co chcesz. Z czubka mojej głowy:
Początkowy rozmiar
vector
i unique_ptr<T[]>
zezwól na określenie rozmiaru w czasie wykonywaniaarray
pozwala tylko określić rozmiar w czasie kompilacjiZmiana rozmiaru
array
i unique_ptr<T[]>
nie zezwalaj na zmianę rozmiaruvector
robiPrzechowywanie
vector
i unique_ptr<T[]>
przechowuj dane poza obiektem (zwykle na stercie)array
przechowuje dane bezpośrednio w obiekcieBiurowy
array
i vector
zezwól na kopiowanieunique_ptr<T[]>
nie zezwala na kopiowanieZamień / przenieś
vector
i unique_ptr<T[]>
mieć O (1) swap
operacje czasu i ruchuarray
ma O (n) swap
operacji ruchu i czasu , gdzie n jest liczbą elementów w tablicyUnieważnienie wskaźnika / odwołania / iteratora
array
zapewnia, że wskaźniki, referencje i iteratory nigdy nie zostaną unieważnione, gdy obiekt jest aktywny, nawet na swap()
unique_ptr<T[]>
nie ma iteratorów; wskaźniki i referencje są unieważniane tylko wtedy, swap()
gdy obiekt jest aktywny. (Po zamianie wskaźniki wskazują na tablicę, którą zamieniłeś, więc nadal są „prawidłowe” w tym sensie).vector
może unieważnić wskaźniki, referencje i iteratory przy każdej realokacji (i zapewnia pewne gwarancje, że realokacja może nastąpić tylko w przypadku niektórych operacji).Zgodność z pojęciami i algorytmami
array
i vector
oba są konteneramiunique_ptr<T[]>
nie jest konteneremMuszę przyznać, że wygląda to na okazję do pewnego refaktoryzacji dzięki projektowi opartemu na zasadach.
vector
. Następnie zwiększasz rozmiar lub pojemność, vector
tak że wymusza to realokację. Wówczas iterator, wskaźnik lub odwołanie nie wskazują już tego elementu vector
. To właśnie rozumiemy przez „unieważnienie”. Ten problem się nie zdarza array
, ponieważ nie ma „realokacji”. Właściwie właśnie zauważyłem jakiś szczegół i zredagowałem go, aby pasował.
unique_ptr<T[]>
ponieważ nie ma realokacji. Ale oczywiście, gdy tablica wykracza poza zakres, wskaźniki do określonych elementów nadal będą unieważniane.
T[]
, rozmiar (lub równoważna informacja) musi gdzieś wisieć, operator delete[]
aby poprawnie zniszczyć elementy tablicy. Byłoby miło, gdyby programista miał do tego dostęp.
Jednym z powodów, dla których możesz użyć, unique_ptr
jest to, że nie chcesz ponosić kosztów w czasie wykonywania inicjalizacji wartości tablicy.
std::vector<char> vec(1000000); // allocates AND value-initializes 1000000 chars
std::unique_ptr<char[]> p(new char[1000000]); // allocates storage for 1000000 chars
std::vector
Konstruktor i std::vector::resize()
będą wartości inicjalizacji T
- ale new
nie zrobi tego, jeśli T
jest POD.
Zobacz Obiekty inicjowane przez wartość w C ++ 11 i konstruktor std :: vector
Zauważ, że vector::reserve
tutaj nie ma alternatywy: czy dostęp do surowego wskaźnika po std :: vector :: Reserve jest bezpieczny?
To z tego samego powodu programista C może zdecydować malloc
się calloc
.
std::vector
w niestandardowym podzielnika który unika budowę typów, które są std::is_trivially_default_constructible
i niszczenie obiektów, które std::is_trivially_destructible
, choć ściśle ten narusza standard C ++ (ponieważ takie typy nie są domyślnie zainicjowane).
std::unique_ptr
nie zapewnia sprawdzanie zakresu przeciwieństwie do wielu std::vector
implementacjach.
std::vector
jest wymagany przez Standard do sprawdzenia granic .at()
. Myślę, że miałeś na myśli, że niektóre implementacje mają tryby debugowania, które również się sprawdzają .operator[]
, ale uważam, że jest to bezużyteczne do pisania dobrego, przenośnego kodu.
std::vector
Mogą być kopiowane dookoła, natomiast unique_ptr<int[]>
umożliwia wyrażanie unikalną własność tablicy. std::array
z drugiej strony wymaga określenia rozmiaru w czasie kompilacji, co może być niemożliwe w niektórych sytuacjach.
unique_ptr
zamiast shared_ptr
. Czy coś brakuje?
unique_ptr
nie tylko zapobiega przypadkowemu niewłaściwemu użyciu. Jest również mniejszy i niższy narzut niż shared_ptr
. Chodzi o to, że chociaż fajnie jest mieć semantykę w klasie, która zapobiega „niewłaściwemu użyciu”, nie jest to jedyny powód, aby używać określonego typu. I vector
jest o wiele bardziej przydatny jako pamięć tablicowa niż unique_ptr<T[]>
, jeśli bez żadnego innego powodu niż fakt, że ma rozmiar .
vector
na unique_ptr<T[]>
ile to możliwe, zamiast po prostu mówiąc: „nie można skopiować go”, a więc wybrać unique_ptr<T[]>
, gdy nie chcesz kopii. Powstrzymanie kogoś przed zrobieniem czegoś złego niekoniecznie jest najważniejszym powodem wyboru klasy.
std::vector
ma więcej narzutów niż a std::unique_ptr
- używa ~ 3 wskaźników zamiast ~ 1. std::unique_ptr
blokuje kopiowanie konstrukcji, ale umożliwia przenoszenie konstrukcji, która jeśli semantycznie dane, z którymi pracujesz, mogą być tylko przenoszone, ale nie kopiowane, infekuje class
zawierające je dane. Operacja na niepoprawnych danych faktycznie pogarsza klasę kontenera, a „po prostu nie używaj jej” nie zmywa wszystkich grzechów. Konieczność umieszczenia każdego wystąpienia std::vector
w klasie, w której ręcznie wyłączasz, move
powoduje ból głowy. std::unique_ptr<std::array>
posiada size
.
Scott Meyers ma to do powiedzenia w Effective Modern C ++
Istnienie
std::unique_ptr
na tablicach powinna być tylko odsetki intelektualnej do ciebie, ponieważstd::array
,std::vector
,std::string
są praktycznie zawsze lepsze wybory struktura danych niż surowe tablic. O jedynej sytuacji, jaką mogę sobie wyobrazić, kiedystd::unique_ptr<T[]>
miałoby sens, byłoby użycie interfejsu API podobnego do C, który zwraca surowy wskaźnik do tablicy sterty, którą przejmujesz na własność.
Myślę, że odpowiedź Charlesa Salvii jest istotna: std::unique_ptr<T[]>
jest to jedyny sposób na zainicjowanie pustej tablicy, której rozmiar nie jest znany w czasie kompilacji. Co Scott Meyers miałby do powiedzenia na temat motywacji do korzystania std::unique_ptr<T[]>
?
vector
stackoverflow.com/a/24852984/2436175 .
W przeciwieństwie do std::vector
i std::array
, std::unique_ptr
może posiadać wskaźnik NULL.
Jest to przydatne podczas pracy z interfejsami API języka C, które oczekują tablicy lub wartości NULL:
void legacy_func(const int *array_or_null);
void some_func() {
std::unique_ptr<int[]> ptr;
if (some_condition) {
ptr.reset(new int[10]);
}
legacy_func(ptr.get());
}
Użyłem unique_ptr<char[]>
do realizacji zdefiniowanej przez pule pamięci wykorzystywane w silniku gry. Chodzi o to, aby zapewnić używane wstępnie przydzielone pule pamięci zamiast dynamicznych przydziałów do zwracania wyników żądań kolizji i innych rzeczy, takich jak fizyka cząstek, bez konieczności przydzielania / zwalniania pamięci w każdej ramce. Jest to dość wygodne w tego rodzaju scenariuszach, w których potrzebujesz pul pamięci do przydzielania obiektów o ograniczonym czasie życia (zwykle jedna, 2 lub 3 klatki), które nie wymagają logiki niszczenia (tylko dezalokacja pamięci).
W niektórych wywołaniach API Windows Win32 można znaleźć wspólny wzorzec , w którym użycie std::unique_ptr<T[]>
może się przydać, np. Gdy nie wiesz dokładnie, jak duży powinien być bufor wyjściowy podczas wywoływania niektórych API Win32 (które zapisują niektóre dane w środku ten bufor):
// Buffer dynamically allocated by the caller, and filled by some Win32 API function.
// (Allocation will be made inside the 'while' loop below.)
std::unique_ptr<BYTE[]> buffer;
// Buffer length, in bytes.
// Initialize with some initial length that you expect to succeed at the first API call.
UINT32 bufferLength = /* ... */;
LONG returnCode = ERROR_INSUFFICIENT_BUFFER;
while (returnCode == ERROR_INSUFFICIENT_BUFFER)
{
// Allocate buffer of specified length
buffer.reset( BYTE[bufferLength] );
//
// Or, in C++14, could use make_unique() instead, e.g.
//
// buffer = std::make_unique<BYTE[]>(bufferLength);
//
//
// Call some Win32 API.
//
// If the size of the buffer (stored in 'bufferLength') is not big enough,
// the API will return ERROR_INSUFFICIENT_BUFFER, and the required size
// in the [in, out] parameter 'bufferLength'.
// In that case, there will be another try in the next loop iteration
// (with the allocation of a bigger buffer).
//
// Else, we'll exit the while loop body, and there will be either a failure
// different from ERROR_INSUFFICIENT_BUFFER, or the call will be successful
// and the required information will be available in the buffer.
//
returnCode = ::SomeApiCall(inParam1, inParam2, inParam3,
&bufferLength, // size of output buffer
buffer.get(), // output buffer pointer
&outParam1, &outParam2);
}
if (Failed(returnCode))
{
// Handle failure, or throw exception, etc.
...
}
// All right!
// Do some processing with the returned information...
...
std::vector<char>
w tych przypadkach.
Napotkałem przypadek, w którym musiałem użyć std::unique_ptr<bool[]>
, który był w bibliotece HDF5 (biblioteka do wydajnego przechowywania danych binarnych, bardzo często używana w nauce). Niektóre kompilatory (w moim przypadku Visual Studio 2015) zapewniają kompresjęstd::vector<bool>
(przy użyciu 8 booli w każdym bajcie), co jest katastrofą dla czegoś takiego jak HDF5, który nie dba o tę kompresję. Dzięki std::vector<bool>
HDF5 w końcu odczytywał śmieci z powodu tej kompresji.
Zgadnij, kto był tam na ratunek, w przypadku, gdy std::vector
nie działał, a ja musiałem przydzielić dynamiczną tablicę w czysty sposób? :-)
W skrócie: jest to najbardziej wydajna pamięć.
A std::string
zawiera wskaźnik, długość i bufor „optymalizacji ciągu krótkiego”. Ale moja sytuacja polega na tym, że muszę przechowywać ciąg prawie zawsze pusty, w strukturze, w której mam setki tysięcy. W C po prostu bym użył char *
i przez większość czasu byłby zerowy. Który działa również dla C ++, z wyjątkiem tego, że char *
nie ma destruktora i nie wie, jak się usunąć. Natomiast a std::unique_ptr<char[]>
usunie się, gdy wyjdzie poza zakres. Pusty std::string
zajmuje 32 bajty, ale pusty std::unique_ptr<char[]>
zajmuje 8 bajtów, czyli dokładnie tyle, ile ma wskaźnik.
Największym minusem jest to, że za każdym razem, gdy chcę poznać długość struny, muszę ją wezwać strlen
.
Aby odpowiedzieć ludziom myślącym, że „musisz” użyć vector
zamiast unique_ptr
mam przypadek w programowaniu CUDA na GPU, kiedy przydzielasz pamięć w urządzeniu, musisz wybrać tablicę wskaźników (z cudaMalloc
). Następnie, podczas pobierania tych danych z hosta, musisz ponownie wybrać wskaźnik i możesz unique_ptr
łatwo obsługiwać wskaźnik. Dodatkowy koszt konwersji double*
na vector<double>
jest zbędny i prowadzi do utraty perf.
Dodatkowy powód, aby zezwolić i używać std::unique_ptr<T[]>
, o którym do tej pory nie wspomniano w odpowiedziach: pozwala na zadeklarowanie w przód typu elementu tablicy.
Jest to przydatne, gdy chcesz zminimalizować łańcuch #include
instrukcje w nagłówkach (aby zoptymalizować wydajność kompilacji).
Na przykład -
myclass.h:
class ALargeAndComplicatedClassWithLotsOfDependencies;
class MyClass {
...
private:
std::unique_ptr<ALargeAndComplicatedClassWithLotsOfDependencies[]> m_InternalArray;
};
myclass.cpp:
#include "myclass.h"
#include "ALargeAndComplicatedClassWithLotsOfDependencies.h"
// MyClass implementation goes here
Dzięki powyższej strukturze kodu każdy może #include "myclass.h"
i może korzystać MyClass
bez konieczności uwzględniania wewnętrznych zależności implementacyjnych wymaganych przez MyClass::m_InternalArray
.
Jeśli m_InternalArray
zamiast tego został zadeklarowany odpowiednio jako std::array<ALargeAndComplicatedClassWithLotsOfDependencies>
, lub a std::vector<...>
- wynikiem byłaby próba użycia niekompletnego typu, co jest błędem czasu kompilacji.
class ALargeAndComplicatedClassWithLotsOfDependencies
. Logicznie więc nie powinieneś napotykać takich scenariuszy.
Nie mogę wystarczająco mocno pogodzić się z duchem przyjętej odpowiedzi. „Narzędzie ostateczności”? Daleko stąd!
Według mnie jedną z najsilniejszych cech C ++ w porównaniu do C i niektórych innych podobnych języków jest zdolność do wyrażania ograniczeń, aby można je było sprawdzić w czasie kompilacji i zapobiec przypadkowemu niewłaściwemu użyciu. Podczas projektowania konstrukcji zadaj sobie pytanie, na jakie operacje powinna ona zezwalać. Wszelkie inne zastosowania powinny być zabronione, a najlepiej, jeśli takie ograniczenia można wdrożyć statycznie (w czasie kompilacji), aby niewłaściwe użycie skutkowało niepowodzeniem kompilacji.
Kiedy więc potrzebna jest tablica, odpowiedzi na następujące pytania określają jej zachowanie: 1. Czy jej rozmiar a) jest dynamiczny w czasie wykonywania lub b) statyczny, ale znany tylko w czasie wykonywania, lub c) statyczny i znany w czasie kompilacji? 2. Czy tablicę można przydzielić na stosie, czy nie?
I na podstawie odpowiedzi jest to, co uważam za najlepszą strukturę danych dla takiej tablicy:
Dynamic | Runtime static | Static
Stack std::vector unique_ptr<T[]> std::array
Heap std::vector unique_ptr<T[]> unique_ptr<std::array>
Tak, myślę, że unique_ptr<std::array>
należy również wziąć pod uwagę, i żadne z nich nie jest narzędziem ostateczności. Pomyśl, co najlepiej pasuje do Twojego algorytmu.
Wszystkie z nich są kompatybilne ze zwykłymi interfejsami API języka C za pomocą surowego wskaźnika do tablicy danych ( vector.data()
/ array.data()
/ uniquePtr.get()
).
PS Oprócz powyższych rozważań, istnieje również własność: std::array
i std::vector
mają semantykę wartości (mają natywne wsparcie dla kopiowania i przekazywania według wartości), podczas gdy unique_ptr<T[]>
mogą być przenoszone tylko (wymusza pojedyncze prawo własności). Oba mogą być przydatne w różnych scenariuszach. Wręcz przeciwnie, zwykłe tablice statyczne ( int[N]
) i zwykłe tablice dynamiczne ( new int[10]
) nie oferują żadnego z nich i dlatego należy ich unikać, jeśli to możliwe - co powinno być możliwe w zdecydowanej większości przypadków. Jeśli to nie wystarczy, zwykłe tablice dynamiczne również nie oferują możliwości sprawdzenia ich wielkości - dodatkowa szansa na uszkodzenie pamięci i dziury w zabezpieczeniach.
Mogą to być najodpowiedniejsze możliwe odpowiedzi, gdy można wcisnąć tylko jeden wskaźnik za pomocą istniejącego interfejsu API (pomyśl komunikat okna lub parametry wywołania zwrotnego związane z wątkami), które mają pewien pomiar czasu życia po „złapaniu” po drugiej stronie włazu, ale niezwiązane z kodem wywołującym:
unique_ptr<byte[]> data = get_some_data();
threadpool->post_work([](void* param) { do_a_thing(unique_ptr<byte[]>((byte*)param)); },
data.release());
Wszyscy chcemy, aby rzeczy były dla nas miłe. C ++ jest po raz drugi.
unique_ptr<char[]>
może być używany tam, gdzie chcesz wydajności C i wygody C ++. Pomyśl, że musisz operować milionami (ok, miliardami, jeśli jeszcze nie ufasz) ciągów. Przechowywanie każdego z nich w osobnym string
lub vector<char>
obiekcie byłoby katastrofą dla procedur zarządzania pamięcią (stertą). Zwłaszcza jeśli trzeba wielokrotnie przydzielać i usuwać różne ciągi.
Można jednak przydzielić jeden bufor do przechowywania tylu ciągów. Nie chciałbyś char* buffer = (char*)malloc(total_size);
z oczywistych powodów (jeśli nie oczywiste, wyszukaj „dlaczego korzystać z inteligentnych plików ptrs”). Woliszunique_ptr<char[]> buffer(new char[total_size]);
Analogicznie te same względy dotyczące wydajności i wygody dotyczą nie- char
danych (rozważ miliony wektorów / macierzy / obiektów).
vector<char>
? Przypuszczam, że odpowiedź jest taka, ponieważ będą one inicjowane zerem podczas tworzenia bufora, a nie będą, jeśli użyjesz unique_ptr<char[]>
. Ale w Twojej odpowiedzi brakuje tego kluczowego samorodka.
new[]
std::vector
, na przykład, aby zapobiec przypadkowemu wprowadzeniu kopii przez nieostrożnych programistówIstnieje ogólna zasada, że kontenery C ++ powinny być preferowane w porównaniu z własnymi wskaźnikami. Jest to ogólna zasada; ma wyjątki. Jest więcej; to tylko przykłady.
Jeśli potrzebujesz dynamicznej tablicy obiektów, których nie da się zbudować z kopii, dobrym wyborem jest inteligentny wskaźnik do tablicy. Na przykład co jeśli potrzebujesz tablicy atomowej.
std::shared_ptr<T[]>
, ale powinno być i prawdopodobnie będzie w C ++ 14, jeśli ktokolwiek będzie miał problem z napisaniem propozycji. W międzyczasie zawsze jestboost::shared_array
.std::shared_ptr
<T []> jest teraz w c ++ 17.