Je sais qu'il n'y a aucun moyen en C++ d'obtenir la taille d'un tableau créé dynamiquement, tel que:
int* a;
a = new int[n];
Ce que je voudrais savoir, c'est: pourquoi? Les gens ont-ils simplement oublié cela dans la spécification de C++, ou y a-t-il une raison technique à cela?
Les informations ne sont-elles pas stockées quelque part? Après tout, la commande
delete[] a;
semble savoir combien de mémoire il doit libérer, il me semble donc que delete[]
a un moyen de connaître la taille de a
.
Vous constaterez souvent que les gestionnaires de mémoire n'allouent de l'espace que dans un certain multiple, 64 octets par exemple.
Ainsi, vous pouvez demander un nouvel int [4], c'est-à-dire 16 octets, mais le gestionnaire de mémoire allouera 64 octets à votre demande. Pour libérer cette mémoire, il n'est pas nécessaire de connaître la quantité de mémoire que vous avez demandée, mais seulement de vous avoir alloué un bloc de 64 octets.
La question suivante peut être, ne peut-il pas stocker la taille demandée? Il s'agit d'une surcharge supplémentaire que tout le monde n'est pas prêt à payer. Un Arduino Uno, par exemple, n'a que 2 Ko de RAM, et dans ce contexte, 4 octets pour chaque allocation deviennent soudainement importants.
Si vous avez besoin de cette fonctionnalité, vous disposez de std :: vector (ou équivalent) ou de langues de niveau supérieur. C/C++ a été conçu pour vous permettre de travailler avec aussi peu de frais généraux que vous choisissez d'utiliser, ceci étant un exemple.
C'est un prolongement de la règle fondamentale de "ne payez pas pour ce dont vous n'avez pas besoin". Dans votre exemple delete[] a;
ne fait pas besoin de connaître la taille du tableau, car int n'a pas de destructeur. Si vous aviez écrit:
std::string* a;
a = new std::string[n];
...
delete [] a;
Ensuite, le delete
doit appeler des destructeurs (et doit savoir combien appeler) - auquel cas le new
doit enregistrer ce nombre. Cependant, étant donné qu'il ne doit être sauvegardé en toutes occasions, Bjarne a décidé de ne pas lui donner accès.
(Avec le recul, je pense que c'était une erreur ...)
Même avec int
bien sûr, quelque chose doit connaître la taille de la mémoire allouée, mais:
De nombreux allocateurs arrondissent la taille à un multiple pratique (disons 64 octets) pour des raisons d'alignement et de commodité. L'allocateur sait qu'un bloc fait 64 octets de long - mais il ne sait pas si c'est parce que n
était 1 ... ou 16.
La bibliothèque d'exécution C++ peut ne pas avoir accès à la taille du bloc alloué. Si par exemple, new
et delete
utilisent malloc
et free
sous le capot, alors la bibliothèque C++ n'a aucun moyen de connaître la taille d'un bloc retourné par malloc
. (Habituellement, bien sûr, new
et malloc
font tous deux partie de la même bibliothèque - mais pas toujours.)
Une raison fondamentale est qu'il n'y a pas de différence entre un pointeur vers le premier élément d'un tableau alloué dynamiquement de T
et un pointeur vers tout autre T
.
Considérons une fonction fictive qui renvoie le nombre d'éléments vers lesquels pointe un pointeur.
Appelons-le "taille".
Ça a l'air vraiment sympa, non?
Si ce n'était pas du fait que tous les pointeurs sont créés égaux:
char* p = new char[10];
size_t ps = size(p+1); // What?
char a[10] = {0};
size_t as = size(a); // Hmm...
size_t bs = size(a + 1); // Wut?
char i = 0;
size_t is = size(&i); // OK?
On pourrait dire que le premier devrait être 9
, la deuxième 10
, le troisième 9
, et le dernier 1
, mais pour cela, vous devez ajouter une "étiquette de taille" sur chaque objet unique.
Un char
nécessitera 128 bits de stockage (en raison de l'alignement) sur une machine 64 bits. C'est seize fois plus que ce qui est nécessaire.
(Ci-dessus, le tableau à dix caractères a
nécessiterait au moins 168 octets.)
Cela peut être pratique, mais c'est aussi trop cher.
Vous pouvez bien sûr imaginer une version qui n'est bien définie que si l'argument est vraiment un pointeur vers le premier élément d'une allocation dynamique par défaut operator new
, mais ce n'est pas aussi utile qu'on pourrait le penser.
Vous avez raison de dire qu'une partie du système devra connaître la taille. Mais obtenir ces informations n'est probablement pas couvert par l'API du système de gestion de la mémoire (pensez malloc
/free
), et la taille exacte que vous avez demandée peut ne pas être connue, car elle peut avoir été arrondie vers le haut.
Il y a un curieux cas de surcharge du operator delete
que je trouvé sous la forme de:
void operator delete[](void *p, size_t size);
Le paramètre taille semble avoir par défaut la taille (en octets) du bloc de mémoire vers lequel pointe void * p. Si cela est vrai, il est raisonnable d'espérer au moins qu'il a une valeur passée par l'invocation de operator new
et, par conséquent, devrait simplement être divisé par sizeof (type) pour fournir le nombre d'éléments stockés dans le tableau.
Quant à la partie "pourquoi" de votre question, la règle de Martin de "ne pas payer pour ce dont vous n'avez pas besoin" semble la plus logique.
Il n'y a aucun moyen de savoir comment vous allez utiliser ce tableau. La taille d'allocation ne correspond pas nécessairement au numéro d'élément, vous ne pouvez donc pas simplement utiliser la taille d'allocation (même si elle était disponible).
C'est une faille profonde dans d'autres langages pas en C++. Vous obtenez la fonctionnalité que vous désirez avec std :: vector tout en conservant un accès brut aux tableaux. Conserver cet accès brut est essentiel pour tout code qui doit réellement effectuer un travail.
Plusieurs fois, vous effectuerez des opérations sur des sous-ensembles du tableau et lorsque vous aurez une comptabilité supplémentaire intégrée dans le langage, vous devrez réallouer les sous-tableaux et copier les données pour les manipuler avec une API qui attend un tableau géré.
Considérez simplement le cas banal du tri des éléments de données. Si vous avez géré des tableaux, vous ne pouvez pas utiliser la récursivité sans copier les données pour créer de nouveaux sous-tableaux à passer récursivement.
Un autre exemple est une FFT qui manipule récursivement les données en commençant par 2x2 "papillons" et retourne à l'ensemble du tableau.
Pour corriger la baie gérée, vous avez maintenant besoin de "autre chose" pour corriger ce défaut et que "autre chose" est appelé "itérateurs". (Vous avez maintenant géré des tableaux mais ne les transmettez presque jamais à des fonctions car vous avez besoin d'itérateurs + 90% du temps.)