4
votes

Comment std :: vector prend en charge la mémoire contiguë pour les objets personnalisés de taille inconnue

J'ai du mal à trouver le bon modèle mental et à comprendre std :: vector .

Ce que je pensais savoir

Lorsque vous créez un vecteur de type T puis réserve N éléments pour le vecteur, le compilateur trouve et réserve essentiellement un bloc contigu de mémoire qui est N * sizeof (T) octets. Par exemple,

class Foo {

public:
    Foo() = default;
    Foo(std::vector<int> vec): _vec{vec} {}

private:
    std::vector<int> _vec;
};

int main() {

    // Initialize a vector Foo
    std::vector<Foo> foovec;

    // Reserve contigious block of 4 ?-byte chunks of memory
    foovec.reserve(4);  // [ | | | ]

    // How does memory allocation work since object sizes are unkown?
    foovec.emplace_back(std::vector<int> {1,2});        // [{1,2}| | | ]
    foovec.emplace_back(std::vector<int> {1,2,3,4,5});  // [{1,2}|{1,2,3,4,5}| | ]

    return 0;
}

Ensuite, nous pouvons accéder à n'importe quel élément en temps d'accès aléatoire car, si nous demandons le kème élément du vecteur, nous commençons simplement à l'adresse mémoire du début du vecteur puis "sauter" k * sizeof (T) octets pour accéder au kème élément.

Objets personnalisés

Mon modèle mental se brise vers le bas pour les objets personnalisés de taille inconnue / variable. Par exemple,

// Initialize a vector of int
std::vector<int> intvec;

// Reserve contigious block of 4 4-byte chunks of memory
intvec.reserve(4);  // [ | | | ]

// Filling in the memory chunks has obvious behavior:
intvec.push_back(1);  // [1| | | ]
intvec.push_back(2);  // [1|2| | ]

Puisque nous ne connaissons pas la taille de chaque instance de Foo, comment foovec.reserve () alloue-t-il de la mémoire? De plus, comment pouvez-vous obtenir un temps d'accès aléatoire, nous ne savons pas jusqu'où "sauter" pour arriver au kème élément?


6 commentaires

désolé, mais votre question est basée sur une fausse prémisse. La taille de votre objet est connue. Considérez que si un objet contient un pointeur, c'est uniquement le pointeur et non ce qu'il pointe qui contribue à la taille


essayez sizeof (Foo) , vous obtiendrez la taille des objets Foo et c'est la même chose pour toutes les instances (quelle que soit la taille du vecteur contenu)


Il fait conceptuellement :: operator new (4 * sizeof (Foo)); . sizeof () est l'opérateur au moment de la compilation


n'ayez pas peur, votre modèle mental n'a pas à se casser ^^


"... le compilateur trouve et réserve essentiellement un bloc de mémoire contigu ..." Non, le runtime trouve la mémoire disponible pour vous. Les vecteurs ont un stockage dynamique. Le compilateur alloue uniquement le stockage pour les objets statiques, optimisations mises à part.


Merci à tous! Je ne savais pas qu'un vecteur est fondamentalement juste un pointeur vers des données et ne contient pas les données elles-mêmes.


4 Réponses :


8
votes

la taille de

class Foo {

public:
    Foo() = default;
    Foo(std::vector<int> vec): _vec{vec} {}

private:
    std::vector<int> _vec;
};

est connue et constante, le std :: vector interne fait l'allocation dans le tas, donc il n'y a pas de problème à faire foovec.reserve (4);

sinon comment un std :: vector peut-il être dans la pile? ;-)


0 commentaires

2
votes

Lorsque vous créez un vecteur de type T puis que vous réservez N éléments pour le vecteur, le compilateur trouve et réserve essentiellement un bloc de mémoire contigu

Le compilateur ne fait rien de tel. Il génère du code pour demander le stockage à l'allocateur du vecteur lors de l'exécution . Par défaut, il s'agit de std :: allocator , qui délègue à operator new , qui récupérera le stockage non initialisé du système d'exécution.

Mon modèle mental se décompose pour les objets personnalisés de taille inconnue / variable

La seule façon dont un type défini par l'utilisateur peut réellement avoir une taille inconnue est s'il est incomplet - et vous ne pouvez pas déclarer un vecteur à un type incomplet.

À tout moment de votre code où le type est complet, sa taille est également fixe, et vous pouvez déclarer un vecteur stockant ce type comme d'habitude.


Votre Foo est complet et sa taille est fixée au moment de la compilation. Vous pouvez vérifier cela avec sizeof (Foo) , et sizeof (foovec [0]) etc.

Le vecteur possède em > une quantité de stockage variable, mais ne la contient pas dans l'objet. Il stocke juste un pointeur et les tailles réservées et utilisées (ou quelque chose d'équivalent). Par exemple, une instance de:

class toyvec {
  int *begin_;
  int *end_;
  size_t capacity_;
public:
  // push_back, begin, end, and all other methods
};

toujours a une taille fixe sizeof (toyvec) = 2 * sizeof (int *) + sizeof (size_t) + might_some_padding . L'allocation d'un énorme bloc de mémoire, et la définition de begin au début de celui-ci, n'a aucun effet sur la taille du pointeur lui-même.


tl; dr C ++ n'a pas d'objets redimensionnés dynamiquement. La taille d'un objet est fixée en permanence par la définition de classe. C ++ possède des objets qui possèdent - et peuvent redimensionner - un stockage dynamique, mais cela ne fait pas partie de l'objet lui-même.


0 commentaires

3
votes

La taille de votre classe Foo est connue au moment de la compilation, la classe std :: vector a une taille constante, car les éléments qu'elle contient sont alloués sur le tas.

std::vector<int> empty{};
std::vector<int> full{};
full.resize(1000000);
assert(sizeof(empty) == sizeof(full));

Les deux instances de std::vector , empty et full seront toujours ont la même taille malgré un nombre différent d'éléments.

Si vous voulez un tableau que vous ne pouvez pas redimensionner, et que sa taille doit être connue au moment de la compilation, utilisez std :: array code>.


0 commentaires

11
votes

Votre conception de la taille est imparfaite. Un std::vector a une taille d'espace connue au moment de la compilation qu'il va prendre. Il a également une taille d'exécution qu'il peut utiliser (elle est allouée au moment de l'exécution et le vecteur contient un pointeur vers elle). Vous pouvez l'imaginer comme

+--------+
|        |
| Vector |
|        |
|        |
+----+---+
     |
     |
     v
+----+----+---------+---------+
| Object  | Object  | Object  |
|  with   |  with   |  with   |
| Vector  | Vector  | Vector  |
+----+----+----+----+----+----+
     |         |         |   +---------+---------+---------+---------+---------+
     |         |         |   |         |         |         |         |         |
     |         |         +-->+ Element | Element | Element | Element | Element |
     |         |             |         |         |         |         |         |
     |         |             +-------------------------------------------------+
     |         |    +-------------------------------------------------+
     |         |    |         |         |         |         |         |
     |         +--->+ Element | Element | Element | Element | Element |
     |              |         |         |         |         |         |
     |              +-------------------------------------------------+
     |    +-------------------------------------------------+
     |    |         |         |         |         |         |
     +--->+ Element | Element | Element | Element | Element |
          |         |         |         |         |         |
          +---------+---------+---------+---------+---------+

Ainsi, lorsque vous avez un vecteur d'objets contenant un vecteur, chaque Elément devient le vecteur puis ces points de à leur propre stockage ailleurs comme

+--------+
|        |
| Vector |
|        |
|        |
+--------+
     |
     |
     v
+-------------------------------------------------+
|         |         |         |         |         |
| Element | Element | Element | Element | Element |
|         |         |         |         |         |
+-------------------------------------------------+

De cette façon, tous les vecteurs sont les uns à côté des autres, mais les éléments que les vecteurs ont peuvent être n'importe où ailleurs dans la mémoire. C'est pour cette raison que vous ne souhaitez pas utiliser un std:vector<:vector>> pour une matrice. Tous les sous-vecteurs obtiennent de la mémoire n'importe où, donc il n'y a pas de localité entre les lignes.


Notez que cela s'applique à tous les conteneurs prenant en charge l'allocateur car ils ne stockent pas les éléments à l'intérieur du conteneur directement. Ce n'est pas vrai pour std :: array car, comme un tableau brut, les éléments font partie du conteneur. Si vous avez un std :: array alors sa taille est d'au moins sizeof (int) * 20 octets.


4 commentaires

Aussi utiles que soient certaines des autres réponses plus détaillées, votre diagramme rend immédiatement évidente la faille de mon modèle mental. Je ne savais pas qu'un vecteur est fondamentalement juste un pointeur vers un tampon alloué de tas comme décrit dans cette réponse . Merci pour ça.


@Ben De rien. Parfois, une bonne image peut être vraiment utile. Content que tu aies aimé.


Il est également utile de comparer std :: array , qui contient directement ses éléments (il y a toujours le même nombre)


@Caleth Ajout d'une note à la fin.