Quel est le type d'une taille en C++?

Plusieurs concepts de taille existent C++ : celle des tableaux primitifs, celle des tableaux alloués dynamiquement, celle des conteneurs standard, celle représentée par la distance entre deux itérateurs ou entre deux pointeurs, et ainsi de suite. Essayons d'y voir clair.

Les tableaux primitifs

Notez qu'il est rare qu'un programme procède à l'inclusion de <cstddef> du fait que la majorité des en-têtes standard y ont déjà recours.

La taille d'un tableau primitif est de type std::size_t. Le type std::size_t n'est pas un type primitif mais est toujours défini dans la bibliothèque standard sous la forme un type non signé défini dans <cstddef> (ou, pour le langage C, dans <stddef.h>).

Le type std::size_t doit être capable de représenter la taille de toute donnée en C++, incluant celle (en bytes) de tout tableau. Il ne faut par contre pas présumer que std::size_t soit égal à unsigned int ou à unsigned long puisque le standard ne prend pas position à cet effet.

L'opérateur sizeof de C et de C++ est de type std::size_t. Cet opérateur est très efficace du fait qu'il est évalué à la compilation et ne coûte donc rien à l'exécution.

Dans le cas d'un tableau automatique ou statique, le nombre d'éléments peut être obtenu à la compilation en divisant la taille du tableau par celle d'un de ses éléments.

#include <cstddef>
#include <iostream>
#include <string>
int main() {
   using namespace std;
   string tab[] { "youppi", "allo", "wow" };
   cout << "Nombre d'éléments de tab: " << sizeof(tab)/ sizeof(tab[0]) << endl;
}

Pour un tableau tab alloué dynamiquement ou dont la taille n'est pas connue localement, sizeof(tab) est toujours la taille d'un pointeur (habituellement 4 du fait que la plupart des ordinateurs au moment d'écrire ceci utilisent des registres 32 bits) et cette manoeuvre ne suffit plus.

#include <cstddef>
#include <iostream>
#include <string>
using namespace std;
//
// Retournera 0 puisque sizeof(tab) == 4 et sizeof(tab[0]) vaudra autour de 28; un
// tableau passé par valeur à une fonction devient, pour cette fonction, un pointeur
//
template <class T>
   constexpr size_t f(const T tab[]) {
      return sizeof(tab) / sizeof(tab[0]);
   }
//
// retournera N, évidemment, mais ne pourra être utilisé que lorsque
// le compilateur connaît la taille du tableau dans le code appelant
//
template <class T, size_t N>
   constexpr size_t g(const T (&tab)[N]) {
      return N;
   }
int main() {
   string tab[] { "youppi", "allo", "wow" };
   cout << "Nombre d'éléments : " << g(tab) << endl;
   //
   // N'a pas de sens
   //
   cout << "Nombre d'éléments : " << f(tab) << endl;
}

À propos de std::size_t, le standard indique :

« The type size_t is an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object » § [support.types.layout]

La distance entre deux pointeurs

Alors qu'une taille, en particulier si elle est représentée par std::size_t, est toujours positive, la distance entre deux pointeurs peut être négative. Le type std::size_t n'est donc pas approprié pour un programme comme le suivant:

#include <cstddef>
#include <iostream>
#include <string>
using namespace std;
int main() {
   string tab[] { "youppi", "allo", "wow" };
   const size_t TAILLE = sizeof(tab) / sizeof(tab[0]);
   string *p;
   for (p = tab + 0; p != tab + TAILLE && (*p)[0] != 'a'; ++p)
      ;
   if (p != tab + TAILLE)
      cout << "Le premier mot débutant par un \'a\' est à l'indice "
           << static_cast<int>(p - tab) << endl;
}

Il faut donc un type distinct pour la distance entre deux pointeurs comme dans le cas de (p - tab) dans l'exemple ci-dessus. Ce type est std::ptrdiff_t.

À propos de std::ptrdiff_t, le standard indique :

« The type ptrdiff_t is an implementation-defined signed integer type that can hold the difference of two subscripts in an array object » § [support.types.layout]

À propos du lien entre std::size_t et std::ptrdiff_t, le standard indique :

[Note: It is recommended that implementations choose types for ptrdiff_t and size_t whose integer conversion ranks [...] are no greater than that of signed long int unless a larger size is necessary to contain all the possible values. –end note]

L'adresse d'une fonction globale ou d'une méthode de classe

L'adresse d'une fonction ou d'une méthode de classe est une adresse, mais ne peut être utilisée à l'aide d'artihmétique de pointeurs. En pratique, l'adresse d'une donnée et l'adresse d'une fonction peuvent ne pas être des pointeurs de même nature (c'était le cas sous DOS / Microsoft Windows jusqu'à l'arrivée de Windows 95).

Les règles quant à l'équivalent de std::size_t et de std::ptrdiff_t existent mais sont dans un volume qui traîne à mon bureau dans le moment (je ne les ai pas sous la main). J'insérerai le nécessaire ici dès que ce sera possible pour moi de le faire.

#include <iostream>
using namespace std;
int f(double);
int main() {
   // ptrf_t est un pointeur sur une fonction int prenant un double en paramètre
   using ptrf_t = int (*)(double);
   ptrf_t pf = f;
   cout << pf(3.14159) << endl;
}

Le type de la fonction int f(double) est int(*)(double). On comprendra que les alias de types avec using (autrefois, avec typedef) soient communs lorsque les pointeurs de fonction apparaissent dans le portrait.

L'adresse d'une méthode d'instance

Les méthodes d'instances suivent le même modèle que les méthodes de classe et les fonctions globales à ceci près qu'elles prennent un paramètre supplémentaire, this, et ne sont donc pas soumis exactement aux mêmes opérateurs. Les tailles impliquées sont les mêmes.

class X {
   const int val;
public:
   X(int val) : val{val} {
   }
   int f() const {
      return val;
   }
};
#include <iostream>
int main() {
   using namespace std;
   // pf est un pointeur sur une méthode d'instance constante
   // d'un X ne prenant pas de paramètre et retournant un int
   using ptrf_t = int (X::*)() const;
   ptrf_t pf = &X::f;
   X x{4};
   cout << (x.*pf)() << endl;
}

La syntaxe est épouvantable, mais ça fonctionne (pour voir un exemple d'utilisation de pointeurs sur des méthodes d'instance, voir cet article).

Les conteneurs standards

Tout conteneur standard (std::list, std::vector, std::string, etc.) définit son propre type interne public pour informer les programmes du sens à donner (entre autres) à la taille du conteneur et à la distance entre deux itérateurs sur des éléments du conteneur.

Dans tous les cas, le type indiquant la taille d'un conteneur C est C::size_type, un type entier non signé. De même, le type indiquant la distance entre deux itérateurs sur des éléments d'un conteneur C est C::difference_type, un type entier signé.

Ainsi, le programme suivant affichera les valeurs 3 et 3. La plupart des éléments un peu particuliers du programme (utilisation de typename, mention explicite de std::vector<std::string> à l'appel de ObtenirDistance()) sont intéressants mais secondaires à notre propos.

#include <vector>
#include <iostream>
#include <string>
#include <algorithm>
#include <iterator>
template <class C>
   typename C::size_type ObtenirTaille(const C &conteneur) {
      typename C::size_type n = conteneur.size(); // ou simplement auto
      return n;
   }
template <class C, class It>
   typename C::difference_type ObtenirNbElements(It debut, It fin) {
      // std::distance() appellera operator- si c'est raisonnable
      typename C::difference_type n = std::distance(debut, fin); // ou simplement auto
      return n;
   }
int main() {
   using namespace std;
   vector<string> v { "youppi", "allo", "wow" };
   cout << "Nombre d'éléments: " << ObtenirTaille(v) << '\n'
        << "Distance entre le dernier élément et le premier élément: "
        << ObtenirNbElements(begin(v), end(v))
        << endl;
}

Définir les concepts importants pour un conteneur à l'aide de spécifications publiques de types est une saine pratique. Vous devriez faire de même avec vos propres types.

La distance entre deux itérateurs

Règle générale, la distance entre deux itérateurs est définie par le type distance_type défini par le conteneur des éléments auxquels ils réfèrent. L'exception tient aux pointeurs dans un tableau qui sont toujours de type std::ptrdiff_t.

Dans le cas d'itérateurs bidirectionnels (ex.: std::vector<T>::iterator), la distance entre deux itérateurs peut être trouvée en temps constant par une simple soustraction, une opération de complexité constante ().

Dans le cas d'un itérateur vers l'avant seulement (ex.: std::list<T>::iterator), la soustraction entre itérateurs n'est pas implémentée parce que nécessairement inefficace: la seule manière d'obtenir la distance est de faire avancer le plus près du début vers le plus près de la fin une position à la fois jusqu'à ce qu'ils soient égaux, une opération de complexité linéaire ().

L'algorithme std::distance(a,b) évalue la distance entre deux itérateurs a et b d'un conteneur donné de la manière la plus efficace possible (temps constant si la soustraction est définie sur ces itérateurs, temps linéaire sinon). Ne l'utilisez qu'à des fins de généralité et en connaissance de cause.


Valid XHTML 1.0 Transitional

CSS Valide !