web-dev-qa-db-fra.com

Pourquoi les classes C ++ sans variables membres occupent-elles de l'espace?

J'ai trouvé que les compilateurs MSVC et GCC allouent au moins un octet pour chaque instance de classe même si la classe est un prédicat sans variable membre (ou avec seulement des variables membres statiques). Le code suivant illustre ce point.

#include <iostream>

class A
{
public:
   bool operator()(int x) const
   {
      return x>0;
   }
};

class B
{
public:
   static int v;
   static bool check(int x)
   {
      return x>0;
   }
};

int B::v = 0;

void test()
{
   A a;
   B b;
   std::cout << "sizeof(A)=" << sizeof(A) << "\n"
             << "sizeof(a)=" << sizeof(a) << "\n"
             << "sizeof(B)=" << sizeof(B) << "\n"
             << "sizeof(b)=" << sizeof(b) << "\n";
}

int main()
{
   test();
   return 0;
}

Production:

sizeof(A)=1
sizeof(a)=1
sizeof(B)=1
sizeof(b)=1

Ma question est pourquoi le compilateur en a-t-il besoin? La seule raison pour laquelle je peux trouver est de garantir que tous les pointeurs var membres diffèrent afin que nous puissions faire la distinction entre deux membres de type A ou B en leur comparant des pointeurs. Mais le coût de cela est assez élevé lorsqu'il s'agit de conteneurs de petite taille. Compte tenu de l'alignement possible des données, nous pouvons obtenir jusqu'à 16 octets par classe sans vars (?!). Supposons que nous ayons un conteneur personnalisé qui contiendra généralement quelques valeurs int. Considérez ensuite un tableau de ces conteneurs (avec environ 1000000 membres). Les frais généraux seront de 16 * 1000000! Un cas typique où cela peut se produire est une classe de conteneur avec un prédicat de comparaison stocké dans une variable membre. De plus, étant donné qu'une instance de classe doit toujours occuper de l'espace, quel type de surcharge doit être attendu lors de l'appel de A()(value)?

52
bkxp

Il est nécessaire de satisfaire un invariant de la norme C++: chaque objet C++ du même type doit avoir une adresse unique pour être identifiable.

Si les objets ne prenaient pas d'espace, les éléments d'un tableau partageraient la même adresse.

74
Konrad Rudolph

Fondamentalement, c'est une interaction entre deux exigences:

  • Deux objets différents du même type doivent se trouver à des adresses différentes.
  • Dans les tableaux, il peut ne pas y avoir de remplissage entre les objets.

Notez que la première condition seule ne nécessite pas de taille non nulle: étant donné

struct empty {};
struct foo { empty a, b; };

la première exigence pourrait facilement être satisfaite en ayant un a de taille nulle suivi d'un octet de remplissage unique pour appliquer une adresse différente, suivi d'un b de taille nulle. Cependant, étant donné

empty array[2];

cela ne fonctionne plus car un remplissage entre les différents objets empty[0] et empty[1] ne serait pas autorisé.

26
celtschk

Tous les objets complets doivent avoir une adresse unique; ils doivent donc prendre au moins un octet de stockage - l'octet à leur adresse.

Un cas typique où cela peut se produire est une classe de conteneur avec un prédicat de comparaison stocké dans une variable membre.

Dans ce cas, vous pouvez utiliser l'optimisation classe de base vide: un sous-objet de base est autorisé à avoir la même adresse que l'objet complet dont il fait partie, il ne peut donc pas prendre de stockage. Vous pouvez donc attacher le prédicat à une classe en tant que classe de base (peut-être privée) plutôt qu'en tant que membre. C'est un peu plus compliqué à gérer qu'un membre, mais cela devrait éliminer les frais généraux.

à quel type de surcharge doit-on s'attendre lors de l'appel de A()(value)?

La seule surcharge par rapport à l'appel d'une fonction non membre passera l'argument supplémentaire this. Si la fonction est en ligne, cela doit être éliminé (comme ce serait le cas, en général, lors de l'appel d'une fonction membre qui n'accède à aucune variable membre).

13
Mike Seymour

Il existe déjà d'excellentes réponses qui répondent à la question principale. Je voudrais répondre aux préoccupations que vous avez exprimées concernant:

Mais le coût de cela est assez élevé lorsqu'il s'agit de conteneurs de petite taille. Compte tenu de l'alignement possible des données, nous pouvons obtenir jusqu'à 16 octets par classe sans vars (?!). Supposons que nous ayons un conteneur personnalisé qui contiendra généralement quelques valeurs int. Considérez ensuite un tableau de ces conteneurs (avec environ 1000000 membres). Les frais généraux seront de 16 * 1000000! Un cas typique où cela peut se produire est une classe de conteneur avec un prédicat de comparaison stocké dans une variable membre.

Éviter le coût de la détention de A

Si toutes les instances d'un conteneur dépendent du type A, il n'est pas nécessaire de conserver des instances de A dans le conteneur. La surcharge associée à la taille non nulle de A peut être évitée en créant simplement une instance de A sur la pile si nécessaire.

Ne pas pouvoir éviter le coût de la détention de A

Vous pouvez être obligé de maintenir un pointeur sur A dans chaque instance du conteneur si A est attendu par polymorphe. Pour un tel conteneur, le coût de chaque conteneur augmente de la taille d'un pointeur. La présence ou non de variables membres dans la classe de base A ne fait aucune différence sur la taille du conteneur.

Impact de sizeof A

Dans les deux cas, la taille d'une classe vide ne devrait pas avoir d'incidence sur les exigences de stockage du conteneur.

1
R Sahu