L'opérateur sizeof

En C++, il arrive que l'on souhaite connaître l'espace occupé par un objet ou par une instance d'un certain type. Pour ce faire, on a accès à l'opérateur sizeof.

L'expression sizeof(T) donne le nombre de bytes utilisé pour représenter une instance du type T. L'expression sizeof obj donne le nombre de bytes utilisé pour représenter l'objet obj. Notez que pour un type, les parenthèses sont obligatoires, alors qu'elles ne le sont pas pour un objet.

L'opérateur sizeof est évalué à la compilation, ce qui permet d'y avoir recours en situation de métaprogrammation.

En C++, un byte est la plus petite unité directement adressable dans l'ordinateur et occupe au moins huit bits.

Sur les objets, le standard de C++ dit ceci (j'ai extrait les références aux diverses sections pour alléger le texte) :

« The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created. An object occupies a region of storage in its period of construction, throughout its lifetime, and in its period of destruction »

Vous comprendrez que pour C++, un humble int est un objet, tout comme l'est une classe des plus complexes.

Quelques trucs intéressants à savoir à ce sujet.

Factoïde Explication Exemples

Le type char représente un byte

En C++, le type char est spécial et sert historiquement pour représenter la mémoire en tant que séquence contiguë de bytes.

static_assert(sizeof(char)==1);
static_assert(sizeof(signed char)==1);
static_assert(sizeof(unsigned char)==1);

Le type bool n'occupe pas nécessairement un byte

En C++, le type bool occupe typiquement un byte, mais cela n'est pas normé. Par contre, les constantes true et false ont des valeurs fixes. Pour des raisons historiques, tout entier non nul est vrai au sens de l'évaluation des conditions, mais cela ne les rend pas équivalents à true

static_assert(static_cast<int>(true)==1);
static_assert(static_cast<int>(false)==0);
static_assert(3);
static_assert(3!=true); // avertissement?

Outre char et ses variantes, les types entiers n'ont pas une taille normée

Ce que nous avons avec les types entiers est une relation entre les tailles.

static_assert(sizeof(short)<=sizeof(int));
static_assert(sizeof(int)<=sizeof(long));
static_assert(sizeof(long)<=sizeof(long long));

Une référence n'a pas de taille en soi

Appliquer sizeof sur une référence donnera le type du référé. Au sens du compilateur, une référence peut n'être qu'un alias vers le référé après tout

class X { /* ... */ };
// ...
X x0{ /* ... */ };
X & r = x0;
static_assert(sizeof r == sizeof(X));

La taille d'un tableau s'évalue à la compilation si le compilateur en est conscient

Il est possible d'exploiter cette information pour fins utiles

int tab[] { 2, 3, 5, 7, 11 };
enum { N = sizeof(tab) / sizeof(tab[0]) };
static_assert(N == 5);

La taille d'un pointeur sur un objet ne dépend pas de la taille du pointé

Un pointeur sur un objet est une adresse, et toutes les adresses sont de même taille. Ceci permet entre autres de réaliser certaines manoeuvres de programmation intéressantes comme l'idiome pImpl

static_assert(sizeof(char)!=sizeof(int));
static_assert(sizeof(char*)==sizeof(int*));
static_assert(sizeof(void*)==sizeof(vector<string>*));
// ...
int i = 3;
char c = 'A';
static_assert(sizeof i != sizeof c);
static_assert(sizeof &i == sizeof &c);

La taille d'un pointeur sur un objet ne correspond pas nécessairement à la taille d'un pointeur de fonction

Le standard ne dicte pas que les pointeurs de fonction soient de même nature que les pointeurs sur des objets. Vaut donc mieux ne pas présumer de telles identités. Techniquement, le standard dit :

« The sizeof operator can be applied to a pointer to a function, but shall not be applied directly to a function »

... et va ensuite en grand détail sur la nature des pointeurs convertibles d'un type à l'autre. Il n'y a pas de garantie offerte quant à la taille d'un pointeur de fonction, et en pratique vous verrez des variations importantes selon les implémentations et les plateformes.

Il existe des alias de types entiers de taille normés dans <cstdint> pour fins d'interopérabilité

Il est malsain de présumer de certaines identités « typiques », comme sizeof(short)==2 ou sizeof(int)==sizeof(void*) qui sont rencontrées fréquemment mais ne sont pas garanties par le standard et peuvent ne pas s'avérer en pratique pour une implémentation donnée. Par contre, si vous souhaitez un type entier d'une certaine taille, la bibliothèque standard offre des alias vers les types appropriés selon les plateformes à travers l'en-tête <cstdint>

static_assert(sizeof(intptr_t)>=sizeof(void*));
static_assert(sizeof(uint16_t)==2); // presumant huit bits par byte
static_assert(sizeof(int_least_16_t)>=2); 

Tout objet occupe au moins un byte d'espace en mémoire

L'idée est que si un type T devait occuper zéro bytes en mémoire, alors dans un tableau T tab[N]; tous les éléments seraient superposés, ce qui rendrait l'arithmétique bien complexe à figurer

struct vide{};
static_assert(sizeof(vide)>=sizeof(char));

Dans une situation d'héritage, si les parents ne contiennent pas d'attributs (de « données membres non-static »), alors le compilateur est en droit « d'aplatir » les parents dans l'enfant

L'idée est que l'enfant occupera alors au moins un byte, donc il est superflu d'en faire croître la taille de la représentation dû à l'ajout d'un sous-objet parent vide. Cette optimisation, très utile en ces temps où les impacts de la Cache dominent la vitesse des programmes, se nomme EBCO (Empty Base Class Optimization)

struct B {};
struct D : B {};
static_assert(sizeof(B) >= 0);
static_assert(sizeof(D) >= 0);
static_assert(sizeof(B) == sizeof(D),
              "Oups, mon compilateur ne supporte pas EBCO");

sizeof...

Il existe aussi une variante, sizeof..., pour obtenir à la compilation le nombre d'éléments d'un Pack (voir templates variadiques).

Son utilisation est assez simple, comme le montre l'exemple à droite. Notez que la valeur exprimée par ces opérateur est le nombre d'éléments dans le Pack, pas leur taille en bytes comme avec sizeof

Contrairement à sizeof, sizeof... impose les parenthèses autour du paramètre, que ce soit avec le type ou avec le Pack. Ainsi :

template <class ... Ts>
   void f(Ts ... args) {
      sizeof...(Ts); // Ok
      sizeof...(args); // Ok
      // sizeof... Ts; // non
      // sizeof... args; // nom
   }
#include <iostream>
using namespace std;
template <class ... Ts>
   void f(Ts ... args) {
      cout << sizeof...(Ts) << endl;
   }
int main() {
   f(2,4,6.5); // 3
   f(); // 0
}

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !