Peut-on utiliser unique_ptr avec array?

238

std::unique_ptr prend en charge les tableaux, par exemple:

std::unique_ptr<int[]> p(new int[10]);

mais est-ce nécessaire? il est probablement plus pratique à utiliser std::vectorou std::array.

Trouvez-vous une utilité pour cette construction?

marais
la source
6
Par souci d'exhaustivité, je dois signaler qu'il n'y en a pas std::shared_ptr<T[]>, mais il devrait y en avoir, et il le sera probablement en C ++ 14 si quelqu'un peut se donner la peine de rédiger une proposition. En attendant, il y en a toujours boost::shared_array.
Pseudonyme
13
std::shared_ptr<T []> est maintenant en c ++ 17.
Vous pouvez trouver plusieurs façons de faire quoi que ce soit sur un ordinateur. Cette construction a une utilité, en particulier dans un chemin chaud, car elle supprime la surcharge des opérations de conteneur si vous savez exactement comment cibler votre tableau. De plus, il crée des tableaux de caractères sans aucun doute de stockage contigu.
kevr

Réponses:

256

Certaines personnes n'ont pas le luxe d'utiliser std::vector, même avec des allocateurs. Certaines personnes ont besoin d'un tableau de taille dynamique std::array. Et certaines personnes obtiennent leurs tableaux à partir d'un autre code connu pour renvoyer un tableau; et ce code ne va pas être réécrit pour retourner unvector ou quelque chose.

En permettant unique_ptr<T[]>, vous répondez à ces besoins.

En bref, vous utilisez unique_ptr<T[]>quand vous en avez besoin . Lorsque les alternatives ne fonctionnent tout simplement pas pour vous. C'est un outil de dernier recours.

Nicol Bolas
la source
27
@NoSenseEtAl: Je ne sais pas quelle partie de "certaines personnes ne sont pas autorisées à le faire" vous échappe. Certains projets ont des exigences très spécifiques, et parmi eux peuvent être "vous ne pouvez pas utiliser vector". Vous pouvez vous demander s'il s'agit d'exigences raisonnables ou non, mais vous ne pouvez pas nier qu'elles existent .
Nicol Bolas
21
Il n'y a aucune raison au monde pour laquelle quelqu'un ne pourrait pas utiliser std::vectors'il le peut std::unique_ptr.
Miles Rout
66
voici une raison de ne pas utiliser vector: sizeof (std :: vector <char>) == 24; sizeof (std :: unique_ptr <char []>) == 8
Arvid
13
@DanNissenbaum Ces projets existent. Certaines industries qui sont soumises à un examen très rigoureux, comme par exemple l'aviation ou la défense, la bibliothèque standard est interdite car il est difficile de vérifier et de prouver qu'elle est correcte pour tout organe directeur fixant les réglementations. Vous pouvez affirmer que la bibliothèque standard est bien testée et je serais d'accord avec vous, mais vous et moi ne faisons pas les règles.
Emily L.
16
@DanNissenbaum De plus, certains systèmes durs en temps réel ne sont pas du tout autorisés à utiliser l'allocation de mémoire dynamique car le retard causé par un appel système peut ne pas être théoriquement limité et vous ne pouvez pas prouver le comportement en temps réel du programme. Ou la limite peut être trop grande, ce qui casse votre limite WCET. Bien que non applicable ici, car ils n'utiliseraient pas non unique_ptrplus, mais ce genre de projets existe vraiment.
Emily L.
125

Il y a des compromis et vous choisissez la solution qui correspond à ce que vous voulez. Du haut de ma tête:

Dimension initiale

  • vectoret unique_ptr<T[]>autoriser la taille à spécifier au moment de l'exécution
  • array permet uniquement de spécifier la taille au moment de la compilation

Redimensionnement

  • arrayet unique_ptr<T[]>ne permettent pas le redimensionnement
  • vector Est-ce que

Espace de rangement

  • vectoret unique_ptr<T[]>stocker les données en dehors de l'objet (généralement sur le tas)
  • array stocke les données directement dans l'objet

Copier

  • arrayet vectorautoriser la copie
  • unique_ptr<T[]> ne permet pas la copie

Échanger / déplacer

  • vectoret unique_ptr<T[]>avoir O (1) temps swapet opérations de déplacement
  • arraya O (n) temps swapet opérations de déplacement, où n est le nombre d'éléments dans le tableau

Invalidation du pointeur / référence / itérateur

  • array garantit que les pointeurs, les références et les itérateurs ne seront jamais invalidés tant que l'objet est en direct, même sur swap()
  • unique_ptr<T[]>n'a pas d'itérateurs; les pointeurs et les références ne sont invalidés swap()que lorsque l'objet est en direct. (Après l'échange, les pointeurs pointent vers le tableau avec lequel vous avez échangé, ils sont donc toujours "valides" dans ce sens.)
  • vector peut invalider des pointeurs, des références et des itérateurs sur toute réallocation (et fournit certaines garanties que la réallocation ne peut se produire que sur certaines opérations).

Compatibilité avec les concepts et algorithmes

  • arrayet vectorsont les deux conteneurs
  • unique_ptr<T[]> n'est pas un conteneur

Je dois admettre que cela ressemble à une opportunité pour une refactorisation avec une conception basée sur des politiques.

Pseudonyme
la source
1
Je ne suis pas sûr de comprendre ce que vous voulez dire dans le contexte de l' invalidation du pointeur . S'agit-il de pointeurs vers les objets eux-mêmes ou de pointeurs vers les éléments? Ou autre chose? Quel genre de garantie obtenez-vous d'un tableau que vous n'obtenez pas d'un vecteur?
jogojapan
3
Supposons que vous ayez un itérateur, un pointeur ou une référence à un élément de a vector. Ensuite, vous augmentez la taille ou la capacité de celui-ci de vectorsorte qu'il force une réallocation. Ensuite, cet itérateur, pointeur ou référence ne pointe plus vers cet élément du vector. C'est ce que nous entendons par "invalidation". Ce problème ne se produit pas array, car il n'y a pas de "réallocation". En fait, je viens de remarquer un détail avec cela, et je l'ai modifié pour convenir.
Pseudonyme
1
Ok, il ne peut pas y avoir d'invalidation à la suite d'une réallocation dans un tableau ou unique_ptr<T[]>parce qu'il n'y a pas de réallocation. Mais bien sûr, lorsque le tableau sort de la portée, les pointeurs vers des éléments spécifiques seront toujours invalidés.
jogojapan
Oui, tous les paris sont désactivés si l'objet n'est plus en vie.
Pseudonyme
1
@rubenvb Bien sûr que vous le pouvez, mais vous ne pouvez pas (disons) utiliser directement des boucles basées sur la plage. Soit dit en passant, contrairement à une normale T[], la taille (ou des informations équivalentes) doit être suspendue quelque part pour operator delete[]détruire correctement les éléments du tableau. Ce serait bien si le programmeur y avait accès.
Pseudonyme
73

Une raison pour laquelle vous pourriez utiliser un unique_ptrest si vous ne voulez pas payer le coût d'exécution de l' initialisation de la valeur du tableau.

std::vector<char> vec(1000000); // allocates AND value-initializes 1000000 chars

std::unique_ptr<char[]> p(new char[1000000]); // allocates storage for 1000000 chars

Le std::vectorconstructeur et std::vector::resize()initialisera la valeur T- mais newne le fera pas s'il Ts'agit d'un POD.

Voir Objets à valeur initialisée en C ++ 11 et constructeur std :: vector

Notez que ce vector::reserven'est pas une alternative ici: L'accès au pointeur brut après std :: vector :: reserve est-il sûr?

C'est la même raison , un programmeur C pourrait choisir mallocplus calloc.

Charles Salvia
la source
Mais cette raison n'est pas la seule solution .
Ruslan
@Ruslan Dans la solution liée, les éléments du tableau dynamique sont toujours initialisés en valeur, mais l'initialisation en valeur ne fait rien. Je conviens qu'un optimiseur qui ne réalise pas que ne rien faire 1000000 fois peut être implémenté par aucun code ne vaut pas un centime, mais on pourrait préférer ne pas dépendre du tout de cette optimisation.
Marc van Leeuwen
encore une autre possibilité est de fournir à std::vectorun allocateur personnalisé qui évite la construction de types qui sont std::is_trivially_default_constructibleet la destruction d'objets qui le sont std::is_trivially_destructible, bien que cela viole strictement la norme C ++ (puisque ces types ne sont pas initialisés par défaut).
Walter
Aussi std::unique_ptrne fournit pas contraire lié à la vérification beaucoup de std::vectormises en œuvre.
diapir
@diapir Il ne s'agit pas de l'implémentation: std::vectorest requis par la norme pour vérifier les limites .at(). Je suppose que vous vouliez dire que certaines implémentations ont des modes de débogage qui seront également enregistrés .operator[], mais je considère que cela est inutile pour écrire un bon code portable.
underscore_d
30

Un std::vectorpeut être copié, tout en unique_ptr<int[]>permettant d'exprimer la propriété unique de la baie. std::array, d'autre part, nécessite que la taille soit déterminée au moment de la compilation, ce qui peut être impossible dans certaines situations.

Andy Prowl
la source
2
Ce n'est pas parce qu'un élément peut être copié qu'il doit l'être.
Nicol Bolas
4
@NicolBolas: Je ne comprends pas. On peut vouloir empêcher cela pour la même raison que l'on utiliserait à la unique_ptrplace de shared_ptr. Suis-je en train de manquer quelque chose?
Andy Prowl
4
unique_ptrfait plus que simplement empêcher une mauvaise utilisation accidentelle. Il est également plus petit et plus bas que shared_ptr. Le fait étant que, même s'il est agréable d'avoir une sémantique dans une classe qui empêche les "abus", ce n'est pas la seule raison d'utiliser un type particulier. Et vectorest beaucoup plus utile en tant que stockage en baie que unique_ptr<T[]>, si ce n'est pour une raison autre que le fait qu'il a une taille .
Nicol Bolas
3
Je pensais avoir clairement expliqué ce point: il y a d' autres raisons d'utiliser un type particulier que cela. Tout comme il y a des raisons de préférer le vectorplus unique_ptr<T[]>possible, au lieu de simplement dire "vous ne pouvez pas le copier" et donc de choisir unique_ptr<T[]>quand vous ne voulez pas de copies. Empêcher quelqu'un de faire la mauvaise chose n'est pas nécessairement la raison la plus importante pour choisir un cours.
Nicol Bolas
8
std::vectora plus de frais généraux qu'un std::unique_ptr- il utilise ~ 3 pointeurs au lieu de ~ 1. std::unique_ptrbloque la construction de copie mais active la construction de déplacement qui, si sémantiquement les données avec lesquelles vous travaillez ne peuvent être déplacées que non copiées, infecte le classcontenant les données. Avoir une opération sur des données non valides aggrave en fait votre classe de conteneur, et "ne l'utilisez pas" ne nettoie pas tous les péchés. Devoir mettre chaque instance de votre std::vectordans une classe où vous désactivez manuellement moveest un casse-tête. std::unique_ptr<std::array>a un size.
Yakk - Adam Nevraumont
22

Scott Meyers a ceci à dire dans Effective Modern C ++

L'existence de std::unique_ptrpour les tableaux devraient être seulement pour vous l' intérêt intellectuel, parce que std::array, std::vector, std::stringsont presque toujours de meilleurs choix de structure de données que les tableaux bruts. À propos de la seule situation que je puisse imaginer quand un std::unique_ptr<T[]>serait logique serait lorsque vous utilisez une API de type C qui renvoie un pointeur brut vers un tableau de tas dont vous assumez la propriété.

Je pense que la réponse de Charles Salvia est pertinente: c'est std::unique_ptr<T[]>le seul moyen d'initialiser un tableau vide dont la taille n'est pas connue au moment de la compilation. Que dirait Scott Meyers de cette motivation à utiliser std::unique_ptr<T[]>?

newling
la source
4
On dirait qu'il n'a tout simplement pas envisagé quelques cas d'utilisation, à savoir un tampon dont la taille est fixe mais inconnue au moment de la compilation, et / ou un tampon pour lequel nous n'autorisons pas les copies. L'efficacité est également une raison possible de la préférer à vector stackoverflow.com/a/24852984/2436175 .
Antonio
17

Contrairement à std::vectoret std::array, std::unique_ptrpeut posséder un pointeur NULL.
Cela est pratique lorsque vous travaillez avec des API C qui attendent un tableau ou 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());
}
George
la source
10

J'ai utilisé unique_ptr<char[]>pour implémenter un pool de mémoire préalloué utilisé dans un moteur de jeu. L'idée est de fournir des pools de mémoire préalloués utilisés au lieu d'allocations dynamiques pour renvoyer les résultats des demandes de collision et d'autres choses comme la physique des particules sans avoir à allouer / libérer de la mémoire à chaque trame. C'est assez pratique pour ce type de scénarios où vous avez besoin de pools de mémoire pour allouer des objets à durée de vie limitée (généralement une, 2 ou 3 images) qui ne nécessitent pas de logique de destruction (uniquement la désallocation de mémoire).

Simon Ferquel
la source
9

Un modèle commun peut être trouvé dans certains appels de l' API Windows Win32 , dans lesquels l'utilisation de std::unique_ptr<T[]>peut être utile, par exemple lorsque vous ne savez pas exactement la taille d'un tampon de sortie lors de l'appel d'une API Win32 (qui écrira des données à l'intérieur ce tampon):

// 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...
...
Mr.C64
la source
Vous pouvez simplement utiliser std::vector<char>dans ces cas.
Arthur Tacca
@ArthurTacca - ... si cela ne vous dérange pas que le compilateur initialise chaque caractère de votre tampon à 0 un par un.
TED
9

J'ai dû faire face à un cas où je devais utiliser std::unique_ptr<bool[]>, qui se trouvait dans la bibliothèque HDF5 (une bibliothèque pour un stockage efficace des données binaires, beaucoup utilisée en science). Certains compilateurs (Visual Studio 2015 dans mon cas) fournissent une compression destd::vector<bool> (en utilisant 8 bools dans chaque octet), ce qui est une catastrophe pour quelque chose comme HDF5, qui ne se soucie pas de cette compression. Avec std::vector<bool>, HDF5 lisait finalement les ordures à cause de cette compression.

Devinez qui était là pour le sauvetage, dans un cas où cela std::vectorn'a pas fonctionné, et j'avais besoin d'allouer proprement un tableau dynamique? :-)

Le physicien quantique
la source
9

En un mot: c'est de loin le plus économe en mémoire.

A std::stringest fourni avec un pointeur, une longueur et un tampon "d'optimisation de chaîne courte". Mais ma situation est que j'ai besoin de stocker une chaîne qui est presque toujours vide, dans une structure dont j'en ai des centaines de milliers. En C, j'utiliserais juste char *, et ce serait nul la plupart du temps. Ce qui fonctionne aussi pour C ++, sauf que a char *n'a pas de destructeur et ne sait pas se supprimer. En revanche, a std::unique_ptr<char[]>se supprimera lorsqu'il sortira du champ d'application. Un vide std::stringprend 32 octets, mais un vide std::unique_ptr<char[]>prend 8 octets, c'est-à-dire exactement la taille de son pointeur.

Le plus gros inconvénient est que chaque fois que je veux connaître la longueur de la chaîne, je dois l'appeler strlen.

jorgbrown
la source
3

Pour répondre aux gens qui pensent que vous "devez" utiliser vectorau lieu de unique_ptrj'ai un cas dans la programmation CUDA sur GPU lorsque vous allouez de la mémoire dans l'appareil, vous devez opter pour un tableau de pointeurs (avec cudaMalloc). Ensuite, lors de la récupération de ces données dans l'hôte, vous devez recommencer pour un pointeur et unique_ptrc'est bien de gérer le pointeur facilement. Le coût supplémentaire de la conversion double*en vector<double>est inutile et entraîne une perte de performance.

Romain Laneuville
la source
3

Une raison supplémentaire d'autoriser et d'utiliser std::unique_ptr<T[]> , qui n'a pas été mentionnée dans les réponses jusqu'à présent: elle vous permet de déclarer en avant le type d'élément du tableau.

Ceci est utile lorsque vous souhaitez minimiser la chaîne #include instructions dans les en-têtes (pour optimiser les performances de génération.)

Par exemple -

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

Avec la structure de code ci-dessus, n'importe qui peut #include "myclass.h"et peut utiliser MyClass, sans avoir à inclure les dépendances d'implémentation internes requises par MyClass::m_InternalArray.

Si a m_InternalArrayété déclaré à la place comme un std::array<ALargeAndComplicatedClassWithLotsOfDependencies>ou un std::vector<...>respectivement - le résultat serait une tentative d'utilisation d'un type incomplet, ce qui est une erreur au moment de la compilation.

Boris Shpungin
la source
Pour ce cas d'utilisation particulier, j'opterais pour le modèle Pimpl pour briser la dépendance - s'il n'est utilisé que de manière privée, la définition peut être différée jusqu'à ce que les méthodes de classe soient implémentées; si elle est utilisée publiquement, les utilisateurs de la classe devraient déjà avoir des connaissances concrètes sur class ALargeAndComplicatedClassWithLotsOfDependencies. Donc, logiquement, vous ne devriez pas rencontrer de tels scénarios.
3

Je ne peux pas être assez en désaccord avec l'esprit de la réponse acceptée. "Un outil de dernier recours"? Loin de là!

Selon moi, l'une des caractéristiques les plus fortes de C ++ par rapport à C et à d'autres langages similaires est la capacité d'exprimer des contraintes afin qu'elles puissent être vérifiées au moment de la compilation et qu'une mauvaise utilisation accidentelle puisse être évitée. Ainsi, lors de la conception d'une structure, demandez-vous quelles opérations elle devrait permettre. Toutes les autres utilisations doivent être interdites, et il est préférable que de telles restrictions puissent être implémentées statiquement (au moment de la compilation) afin qu'une mauvaise utilisation entraîne un échec de compilation.

Ainsi, lorsque l'on a besoin d'un tableau, les réponses aux questions suivantes spécifient son comportement: 1. Sa taille est-elle a) dynamique au moment de l'exécution, ou b) statique, mais uniquement connue au moment de l'exécution, ou c) statique et connue au moment de la compilation? 2. Le tableau peut-il être alloué sur la pile ou non?

Et sur la base des réponses, voici ce que je considère comme la meilleure structure de données pour un tel tableau:

       Dynamic     |   Runtime static   |         Static
Stack std::vector      unique_ptr<T[]>          std::array
Heap  std::vector      unique_ptr<T[]>     unique_ptr<std::array>

Oui, je pense unique_ptr<std::array> devrait également être pris en considération, et ni l'un ni l'autre n'est un outil de dernier recours. Pensez simplement à ce qui correspond le mieux à votre algorithme.

Tous ces éléments sont compatibles avec les API C simples via le pointeur brut vers le tableau de données ( vector.data()/ array.data()/ uniquePtr.get()).

PS En dehors des considérations ci-dessus, il y a aussi une propriété: std::arrayet std::vectoront une sémantique de valeur (ont un support natif pour la copie et le passage par valeur), alors unique_ptr<T[]>qu'elles ne peuvent être déplacées (applique la propriété unique). Les deux peuvent être utiles dans différents scénarios. Au contraire, les tableaux statiques simples ( int[N]) et les tableaux dynamiques simples ( new int[10]) n'offrent ni l'un ni l'autre et doivent donc être évités si possible - ce qui devrait être possible dans la grande majorité des cas. Si cela ne suffisait pas, les tableaux dynamiques simples n'offrent également aucun moyen d'interroger leur taille - une opportunité supplémentaire pour les corruptions de mémoire et les failles de sécurité.

Girafe violette
la source
2

Ils peuvent être la meilleure réponse possible lorsque vous ne piquez qu'un seul pointeur via une API existante (pensez à un message de fenêtre ou à des paramètres de rappel liés au threading) qui ont une certaine durée de vie après avoir été "pris" de l'autre côté de la trappe, mais qui n'est pas lié au code appelant:

unique_ptr<byte[]> data = get_some_data();

threadpool->post_work([](void* param) { do_a_thing(unique_ptr<byte[]>((byte*)param)); },
                      data.release());

Nous voulons tous que les choses soient agréables pour nous. C ++ est pour les autres fois.

Simon Buchan
la source
2

unique_ptr<char[]>peut être utilisé là où vous voulez les performances de C et la commodité de C ++. Considérez que vous devez opérer sur des millions (ok, des milliards si vous ne faites pas encore confiance) de chaînes. Le stockage de chacun d'eux dans un objet stringou un élément distinct vector<char>serait un désastre pour les routines de gestion de la mémoire (tas). Surtout si vous devez allouer et supprimer plusieurs fois différentes chaînes.

Cependant, vous pouvez allouer un seul tampon pour stocker autant de chaînes. Vous char* buffer = (char*)malloc(total_size);n'aimeriez pas pour des raisons évidentes (si ce n'est pas évident, recherchez "pourquoi utiliser des ptr intelligents"). Vous préférezunique_ptr<char[]> buffer(new char[total_size]);

Par analogie, les mêmes considérations de performances et de commodité s'appliquent aux non- chardonnées (considérons des millions de vecteurs / matrices / objets).

Serge Rogatch
la source
On ne les met pas tous dans un grand vector<char>? La réponse, je suppose, est parce qu'ils seront initialisés à zéro lorsque vous créez le tampon, alors qu'ils ne le seront pas si vous utilisez unique_ptr<char[]>. Mais ce nugget clé est absent de votre réponse.
Arthur Tacca
2
  • Vous avez besoin que votre structure contienne juste un pointeur pour des raisons de compatibilité binaire.
  • Vous devez vous interfacer avec une API qui renvoie la mémoire allouée avec new[]
  • Votre entreprise ou votre projet a une règle générale contre l'utilisation std::vector, par exemple, pour empêcher les programmeurs imprudents d'introduire accidentellement des copies
  • Vous voulez empêcher les programmeurs imprudents d'introduire accidentellement des copies dans cette instance.

Il existe une règle générale selon laquelle les conteneurs C ++ doivent être préférés au roulage avec des pointeurs. C'est une règle générale; il a des exceptions. Il y a plus; ce ne sont que des exemples.

Jimmy Hartzell
la source
0

Si vous avez besoin d'un tableau dynamique d'objets qui ne sont pas constructibles par copie, alors un pointeur intelligent vers un tableau est le chemin à parcourir. Par exemple, que faire si vous avez besoin d'un tableau d'atomes.

Ilia Minkin
la source