J'ai lu que std::vector
Devrait être contigu. Je crois comprendre que ses éléments doivent être stockés ensemble et non répartis dans la mémoire. J'ai simplement accepté le fait et utilisé cette connaissance lorsque, par exemple, sa méthode data()
a été utilisée pour obtenir la mémoire contiguë sous-jacente.
Cependant, je suis tombé sur une situation où la mémoire du vecteur se comporte de manière étrange:
std::vector<int> numbers;
std::vector<int*> ptr_numbers;
for (int i = 0; i < 8; i++) {
numbers.Push_back(i);
ptr_numbers.Push_back(&numbers.back());
}
Je m'attendais à ce que cela me donne un vecteur de quelques chiffres et un vecteur de pointeurs sur ces nombres. Cependant, lorsque vous répertoriez le contenu des pointeurs ptr_numbers
, Il existe des nombres différents et apparemment aléatoires, comme si j'accédais à de mauvaises parties de la mémoire.
J'ai essayé de vérifier le contenu à chaque étape:
for (int i = 0; i < 8; i++) {
numbers.Push_back(i);
ptr_numbers.Push_back(&numbers.back());
for (auto ptr_number : ptr_numbers)
std::cout << *ptr_number << std::endl;
std::cout << std::endl;
}
Le résultat ressemble à peu près à ceci:
1
some random number
2
some random number
some random number
3
Il semble donc que lorsque je Push_back()
passe au vecteur numbers
, ses éléments les plus anciens changent d’emplacement.
Alors qu'est-ce que cela signifie exactement, que std::vector
Est un conteneur contigu et que ses éléments bougent? Est-ce que cela les stocke peut-être ensemble, mais les déplace-t-il quand plus d'espace est nécessaire?
Edit: Est-ce que std::vector
Est contiguë seulement depuis C++ 17? (Juste pour que les commentaires sur ma revendication précédente soient pertinents pour les futurs lecteurs.)
Cela ressemble à peu près à ceci (excusez mon chef d’œuvre MS Paint):
L'instance std::vector
Que vous avez sur la pile est un petit objet contenant un pointeur sur un tampon alloué par tas, ainsi que des variables supplémentaires permettant de suivre la taille et la capacité du vecteur.
Il semble donc que lorsque je
Push_back()
passe au vecteurnumbers
, ses éléments les plus anciens changent d’emplacement.
La mémoire tampon allouée au tas a une capacité fixe. Lorsque vous atteignez la fin du tampon, un nouveau tampon sera alloué quelque part sur le tas et tous les éléments précédents seront déplacés dans le nouveau. . Leurs adresses vont donc changer.
Est-ce que cela les stocke peut-être ensemble, mais les déplace-t-il quand plus d'espace est nécessaire?
Grosso modo, oui. La stabilité des éléments avec les itérateurs et les adresses est garantie avec std::vector
uniquement si aucune réallocation n’a lieu.
Je suis conscient que
std::vector
Est un conteneur contigu seulement depuis C++ 17
La structure de la mémoire de std::vector
N'a pas changé depuis sa première apparition dans la norme. ContiguousContainer
est simplement un "concept" qui a été ajouté pour différencier les conteneurs contigus des autres au moment de la compilation.
C'est un seul stockage contigu (un tableau 1d). Chaque fois qu'il manque de capacité, il est réaffecté et les objets stockés sont déplacés vers le nouvel emplacement plus grand. C'est pourquoi vous observez que les adresses des objets stockés changent.
Cela a toujours été comme ça, pas depuis C++17
.
Le stockage s'agrandit géométriquement pour garantir l'exigence de l'amorti O(1)
Push_back()
. Le facteur de croissance est 2 (Casquetten + 1= Casquetten+ Casquetten) dans la plupart des implémentations de la bibliothèque standard C++ ( GCC , Clang , STLPort ) et 1.5 (Casquetten + 1= Casquetten+ Casquetten/ 2) dans la variante MSVC .
Si vous pré-allouez-le avec vector::reserve(N)
et une taille suffisamment grande N
, les adresses des objets stockés ne seront pas modifiées lorsque vous en ajouterez de nouveaux.
Dans la plupart des applications pratiques, il vaut généralement la peine de le pré-allouer à au moins 32 éléments pour éviter les premières réaffectations qui se succèdent (0 → 1 → 2 → 4 → 8 → 16).
Il est aussi parfois pratique de le ralentir, de passer à la politique de croissance arithmétique (Casquetten + 1= Casquetten+ Const), ou arrêtez-vous complètement après une taille assez grande pour vous assurer que l’application ne gaspille pas et ne croît pas en mémoire.
Enfin, dans certaines applications pratiques, telles que les stockages d’objets à base de colonnes, il peut être utile d’abandonner complètement l’idée de stockage contigu au profit d’un stockage segmenté (comme ce que fait std::deque
Mais avec des morceaux beaucoup plus volumineux). De cette façon, les données peuvent être stockées de manière raisonnablement bien localisée pour les requêtes par colonne et par ligne (bien que cela puisse également nécessiter l’aide de l’allocateur de mémoire).
std::vector
être un conteneur contigu signifie exactement ce que vous pensez que cela signifie.
Cependant, de nombreuses opérations sur un vecteur peuvent repositionner l'intégralité de la mémoire.
Un cas courant est lorsque vous ajoutez un élément à celui-ci, le vecteur doit croître, il peut réaffecter et copier tous les éléments dans un autre morceau de mémoire contigu.
Alors qu'est-ce que cela signifie exactement, que std :: vector est un conteneur contigu et pourquoi ses éléments bougent-ils? Est-ce que cela les stocke peut-être ensemble, mais les déplace-t-il quand plus d'espace est nécessaire?
C'est exactement comment cela fonctionne et pourquoi l'ajout d'éléments invalide en effet tous les itérateurs ainsi que les emplacements de mémoire lorsqu'une réallocation a lieu¹. Ce n'est pas seulement valable depuis C++ 17, c'est le cas depuis.
Cette approche présente plusieurs avantages:
data()
peut être utilisée pour transmettre la mémoire brute sous-jacente aux API qui fonctionnent avec des pointeurs bruts.Push_back
, reserve
ou resize
se réduit à temps constant, car la croissance géométrique s’amortit avec le temps (chaque fois que Push_back
appelé la capacité est doublée dans libc ++ et libstdc ++, et une croissance environ d’un facteur 1,5 dans MSVC).Ces implications peuvent être considérées comme les inconvénients d’une telle configuration mémoire:
Push_front
(Comme std::list
Ou std::deque
Fournir) ne sont pas fournies (insert(vec.begin(), element)
fonctionne, mais est peut-être coûteuse¹), ainsi qu'une fusion efficace/épissage de plusieurs instances vectorielles.¹ Merci à @ FrançoisAndrieux de l'avoir signalé.
En termes de structure réelle, un std::vector
ressemble à ceci en mémoire:
struct vector { // Simple C struct as example (T is the type supplied by the template)
T *begin; // vector::begin() probably returns this value
T *end; // vector::end() probably returns this value
T *end_capacity; // First non-valid address
// Allocator state might be stored here (most allocators are stateless)
};
extrait de code pertinent de la libc++
implémentation utilisée par LLVM
Imprimer le contenu brut de la mémoire d'un std::vector
:
(Ne faites pas ceci si vous ne savez pas ce que vous faites!)
#include <iostream>
#include <vector>
struct vector {
int *begin;
int *end;
int *end_capacity;
};
int main() {
union vecunion {
std::vector<int> stdvec;
vector myvec;
~vecunion() { /* do nothing */ }
} vec = { std::vector<int>() };
union veciterator {
std::vector<int>::iterator stditer;
int *myiter;
~veciterator() { /* do nothing */ }
};
vec.stdvec.Push_back(1); // Add something so we don't have an empty vector
std::cout
<< "vec.begin = " << vec.myvec.begin << "\n"
<< "vec.end = " << vec.myvec.end << "\n"
<< "vec.end_capacity = " << vec.myvec.end_capacity << "\n"
<< "vec's size = " << vec.myvec.end - vec.myvec.begin << "\n"
<< "vec's capacity = " << vec.myvec.end_capacity - vec.myvec.begin << "\n"
<< "vector::begin() = " << (veciterator { vec.stdvec.begin() }).myiter << "\n"
<< "vector::end() = " << (veciterator { vec.stdvec.end() }).myiter << "\n"
<< "vector::size() = " << vec.stdvec.size() << "\n"
<< "vector::capacity() = " << vec.stdvec.capacity() << "\n"
;
}