Je n'ai pas beaucoup utilisé le C ces dernières années. Quand j'ai lu cette question aujourd'hui, je suis tombé sur une syntaxe en C que je ne connaissais pas bien.
Apparemment dans C99 , la syntaxe suivante est valide:
void foo(int n) {
int values[n]; //Declare a variable length array
}
Cela semble être une fonctionnalité très utile. A-t-on déjà discuté de l'ajout à la norme C++ et, dans l'affirmative, pourquoi l'a-t-on omis?
Quelques raisons potentielles:
La norme C++ stipule que la taille du tableau doit être une expression constante (8.3.4.1).
Oui, bien sûr, je réalise que dans l'exemple de jouet, on pourrait utiliser std::vector<int> values(m);
, mais cela alloue de la mémoire à partir du tas et non de la pile. Et si je veux un tableau multidimensionnel comme:
void foo(int x, int y, int z) {
int values[x][y][z]; // Declare a variable length array
}
la version vector
devient assez maladroite:
void foo(int x, int y, int z) {
vector< vector< vector<int> > > values( /* Really painful expression here. */);
}
Les tranches, les lignes et les colonnes seront également potentiellement réparties dans la mémoire.
En regardant la discussion à comp.std.c++
il est clair que cette question est assez controversée avec des noms très lourds des deux côtés de l’argument. Il n'est certainement pas évident qu'un std::vector
soit toujours une meilleure solution.
Il y a eu récemment une discussion à propos de ce coup d'envoi dans usenet: Pourquoi pas de VLA en C++ 0x .
Je suis d'accord avec les personnes qui semblent être d'accord pour dire qu'il n'est pas bon de créer un grand tableau potentiel sur la pile, qui ne dispose généralement que de peu d'espace. L'argument est que, si vous connaissez la taille à l'avance, vous pouvez utiliser un tableau statique. Et si vous ne connaissez pas la taille au préalable, vous écrirez du code non sécurisé.
Les VLA C99 peuvent offrir un petit avantage, car ils permettent de créer de petits tableaux sans gaspiller d’espace ni d’appeler des constructeurs pour les éléments inutilisés, mais ils introduiront des modifications assez importantes dans le système de types (vous devez pouvoir spécifier des types en fonction des valeurs d’exécution - this n'existe pas encore dans le langage C++ actuel, à l'exception des spécificateurs de type d'opérateur new
, mais ils font l'objet d'un traitement particulier, de sorte que le délai d'exécution n'échappe pas à la portée de l'opérateur new
.
Vous pouvez utiliser std::vector
, mais ce n'est pas tout à fait pareil, car il utilise la mémoire dynamique, et le faire utiliser son propre allocateur de pile n'est pas vraiment facile (l'alignement est également un problème). Cela ne résout pas non plus le même problème, car un vecteur est un conteneur redimensionnable, alors que les VLA sont de taille fixe. La proposition C++ Dynamic Array a pour but d’introduire une solution basée sur une bibliothèque, à la place d’un VLA basé sur un langage. Cependant, cela ne fera pas partie de C++ 0x, à ma connaissance.
(Contexte: j'ai une certaine expérience de la mise en oeuvre de compilateurs C et C++.)
Les tableaux de longueur variable dans C99 étaient fondamentalement une fausse étape. Afin de soutenir les VLA, C99 a dû faire les concessions suivantes au sens commun:
sizeof x
n'est plus toujours une constante de compilation; le compilateur doit parfois générer du code pour évaluer une expression sizeof
- au moment de l'exécution.
Autoriser les VLA à deux dimensions (int A[x][y]
) nécessitait une nouvelle syntaxe pour déclarer les fonctions prenant les VLA 2D comme paramètres: void foo(int n, int A[][*])
.
Moins important dans le monde C++, mais extrêmement important pour le public cible des programmeurs de systèmes intégrés de C, déclarer un VLA signifie chomping un bloc arbitrairement grand de votre pile. Ceci est un garanti pile-débordement et crash. (Chaque fois que vous déclarez int A[n]
, vous affirmez implicitement que vous avez 2 Go de pile à revendre. Après tout, si vous savez que "n
est certainement inférieur à 1000 ici", vous déclareriez simplement int A[1000]
. Remplacer le nombre entier 32 bits n
par 1000
signifie que vous n’avez aucune idée du comportement de votre programme.)
Bon, passons maintenant au C++. En C++, nous avons la même distinction forte entre "système de types" et "système de valeurs" que C89… mais nous avons vraiment commencé à nous y fier de manière différente de celle de C. Par exemple:
template<typename T> struct S { ... };
int A[n];
S<decltype(A)> s; // equivalently, S<int[n]> s;
Si n
n'était pas une constante de compilation (c'est-à-dire, si A
était de type modifié de manière variable), alors quel serait le type de S
? Le type de S
aussi sera-t-il déterminé uniquement à l'exécution?
Et ça:
template<typename T> bool myfunc(T& t1, T& t2) { ... };
int A1[n1], A2[n2];
myfunc(A1, A2);
Le compilateur doit générer du code pour une instanciation de myfunc
. À quoi ce code devrait-il ressembler? Comment pouvons-nous générer ce code de manière statique si nous ne connaissons pas le type de A1
au moment de la compilation?
Pire encore, que se passe-t-il si, au moment de l'exécution, n1 != n2
, de sorte que !std::is_same<decltype(A1), decltype(A2)>()
? Dans ce cas, l'appel à myfunc
ne devrait même pas être compilé , car la déduction de type de modèle devrait échouer! Comment pourrions-nous imiter ce comportement à l'exécution?
Fondamentalement, C++ évolue dans le sens de la multiplication des décisions dans compile-time: génération de code modèle, constexpr
évaluation des fonctions, etc. Pendant ce temps, C99 était occupé à insérer traditionnellement les décisions compilation (par exemple, sizeof
) dans le runtime. Dans cet esprit, est-il vraiment logique de déployer des efforts en essayant d'intégrer des VLA de type C99 en C++?
Comme tous les autres intervenants l'ont déjà souligné, C++ fournit de nombreux mécanismes d'allocation de tas (std::unique_ptr<int[]> A = new int[n];
ou std::vector<int> A(n);
étant les plus évidents) lorsque vous voulez réellement transmettre l'idée "Je ne sais pas à quel point RAM j'en aurais peut-être besoin. " Et C++ fournit un modèle astucieux de gestion des exceptions pour traiter la situation inévitable selon laquelle la quantité de RAM dont vous avez besoin est supérieure à celle de RAM. Mais j'espère que this la réponse vous donne une bonne idée de la raison pour laquelle les VLA de style C99 n'étaient pas un bon ajustement pour C++ - et non vraiment même un bon ajustement pour C99. ;)
Pour en savoir plus sur le sujet, voir N3810 "Alternatives pour les extensions de tableau" , document de Bjarne Stroustrup d'octobre 2013 sur les VLA. Le point de vue de Bjarne est très différent du mien; N3810 se concentre plus sur la recherche d'un bon C++ ish syntaxe pour les choses, et sur le fait de décourager l'utilisation de tableaux bruts en C++, alors que je me suis davantage concentré sur les implications pour la métaprogrammation et le système de types. Je ne sais pas s'il considère les implications de métaprogrammation/système de types résolues, résolvables ou simplement inintéressantes.
Vous pouvez toujours utiliser alloca () pour allouer de la mémoire sur la pile lors de l'exécution, si vous le souhaitez:
void foo (int n)
{
int *values = (int *)alloca(sizeof(int) * n);
}
Etre alloué sur la pile implique qu'il sera automatiquement libéré lorsque la pile se déroulera.
Remarque rapide: comme indiqué dans la page de manuel Mac OS X pour alloca (3), "la fonction alloca () dépend de la machine et du compilateur; son utilisation est découragée." Juste pour que vous sachiez.
Dans mon propre travail, je me suis rendu compte que chaque fois que je voulais quelque chose comme des tableaux automatiques de longueur variable ou alloca (), le fait que la mémoire se trouve physiquement sur la pile de l'unité centrale me importait peu, juste le fait qu'elle vienne de un allocateur de pile qui n’a pas subi de lenteur de parcours vers le tas général. Donc, j'ai un objet par thread qui possède une mémoire à partir de laquelle il peut Push/Pop tampons de taille variable. Sur certaines plates-formes, je permets que cela se développe via mmu. Les autres plates-formes ont une taille fixe (généralement accompagnée d’une pile cpu de taille fixe, car aucun mmu). Une plate-forme avec laquelle je travaille (une console de jeu portable) possède de toute façon une petite pile de processeurs précieuse car elle réside dans une mémoire rare et rapide.
Je ne dis pas qu'il n'est jamais nécessaire de placer des tampons de taille variable sur la pile de l'unité centrale. Honnêtement, j’ai été surpris de constater que ce n’était pas standard, car il semble que le concept s’intègre assez bien dans le langage. Pour moi cependant, les exigences "taille variable" et "doivent être physiquement situées sur la pile de processeurs" n'ont jamais été réunies. Cela a été une question de vitesse, alors j'ai créé ma propre sorte de "pile parallèle pour les tampons de données".
Il existe des situations où l’allocation de mémoire est très coûteuse par rapport aux opérations effectuées. Un exemple est la matrice mathématique. Si vous travaillez avec de petites matrices dites 5 à 10 éléments et faites beaucoup d’arithmétique, les frais généraux de malloc seront très importants. Dans le même temps, faire de la taille une constante de temps de compilation semble être une perte de temps et un manque de souplesse.
Je pense que C++ est tellement dangereux en soi que l'argument "essayer de ne pas ajouter d'autres fonctionnalités peu sûres" n'est pas très fort. D'un autre côté, le C++ étant sans aucun doute les fonctionnalités de langage de programmation les plus efficaces au moment de l'exécution, il est donc toujours utile: les personnes qui écrivent des programmes critiques en termes de performances utilisent dans une large mesure le C++ et ont besoin de autant de performances que possible. Déplacer des objets d'un tas à l'autre est une telle possibilité. Réduire le nombre de blocs de tas en est un autre. Autoriser les VLA en tant que membres objets serait un moyen d'y parvenir. Je travaille sur une telle suggestion. C'est un peu compliqué à mettre en œuvre, certes, mais cela semble tout à fait faisable.
On dirait qu'il sera disponible en C++ 14:
https://en.wikipedia.org/wiki/C%2B%2B14#Runtime-sized_one_dimensional_arrays
Mise à jour: il n'a pas été intégré à C++ 14.
Cela a été considéré pour l'inclusion dans C++/1x, mais a été abandonné (c'est une correction à ce que j'ai dit plus tôt).
De toute façon, cela serait moins utile en C++ puisque nous avons déjà std::vector
pour remplir ce rôle.
Utilisez std :: vector pour cela. Par exemple:
std::vector<int> values;
values.resize(n);
La mémoire sera allouée sur le tas, mais cela ne représente qu'un petit inconvénient en termes de performances. De plus, il est sage de ne pas allouer de gros blocs de données sur la pile, car sa taille est plutôt limitée.
C99 permet VLA. Et cela impose certaines restrictions sur la façon de déclarer VLA. Pour plus de détails, voir 6.7.5.2 de la norme. C++ interdit VLA. Mais g ++ le permet.
Des tableaux comme celui-ci font partie de C99, mais pas du C++ standard. comme d'autres l'ont déjà dit, un vecteur est toujours une solution bien meilleure, ce qui explique probablement pourquoi les tableaux de taille variable ne figurent pas dans la norme C++ (ni dans la norme C++ 0x proposée).
En passant, pour les questions sur "pourquoi" le standard C++ est ce qu'il est, le groupe de discussion Usenet modéré comp.std.c ++ est l'endroit où aller.