Les tailles en C++ (std::size_t)
- Pourquoi ne pas utiliser int ou unsigned int ?
- Héritage du C: size_t
- std::size_t en C++
- Les pièges du typage
- Les alternatives signées en C: ssize_t et ptrdiff_t
- Les alternatives signées en C++: std::ssize et std::ptrdiff_t
- Dans la STL (Standard Template Library)
- Literals (Depuis C++23)
- Le cas particulier de Qt: qsizetype
- Les recommandations contradictoires de C++ Core Guidelines
En C++, typer les tailles et index avec int ou unsigned int est une erreur fréquente. Le type canonique est std::size_t.
Pourquoi ne pas utiliser int ou unsigned int ?
Sur la majorité des systèmes 64 bits modernes, les types int et unsigned int restent stockés sur 32 bits (modèles LP64 ou LLP64). Cela limite leur valeur maximale: environ 2 milliards pour le signé et 4 milliards pour le non signé.
L’utilisation d’un unsigned int pour itérer sur un grand tableau ou un fichier dépassant 4 Go provoquera un débordement (overflow):
std::vector<char> largeVector(5'000'000'000); // 5 Go (dépasse les 4,2 milliards d'un uint32)
// DANGEREUX: i débordera avant d'atteindre la fin (boucle infinie ou crash)
for (unsigned int i = 0; i < largeVector.size(); ++i) { ... }À l’inverse, std::size_t est garanti d’être assez large pour adresser la plus grande zone mémoire possible sur votre architecture (64 bits sur un système 64 bits).
Héritage du C: size_t
Avant le C++, le langage C a introduit size_t (via <stddef.h>) comme type standard pour toute manipulation de tailles mémoire. Le standard POSIX (fondé sur le C) l’utilise dans ses propres interfaces, notamment via le header système <sys/types.h>.
Il est omniprésent dans la bibliothèque standard C:
-
Allocation:
void* malloc(size_t size); -
Mémoire:
void* memcpy(void* dest, const void* src, size_t n); -
Chaînes:
size_t strlen(const char* s); -
Entrées/Sorties:
fread,fwrite,strncmp, etc.
Usage sémantique: Bien qu’originellement lié à la mémoire,
size_test le type sémantiquement correct pour toute variable représentant une quantité ou un comptage qui ne peut pas être négatif. Si le C historique a souvent utiliséintouunsigned intpar simplicité (au risque d’erreurs de signe ou de dépassement), l’usage desize_test préférable pour la portabilité et la clarté.
Contrairement aux hypothèses fréquentes, le standard n’impose pas de type sous-jacent fixe (comme unsigned int ou unsigned long long).
Selon la norme C (C99):
- C’est un type entier non signé
- Sa largeur est d’au moins 16 bits selon la norme C99 (“The bit width of size_t is not less than 16. (since C99)”)
- Il est suffisant pour représenter la taille maximale d’un objet (typiquement un tableau) supporté par le système
- C’est le type de retour des opérateurs
sizeof,_Alignof(introduit en C11) etalignof(mot-clé préféré depuis C23).
La liberté est donc volontairement laissée aux compilateurs de choisir le type sous-jacent derrière size_t. On dit qu’il est implementation-defined.
Windows a également créé le sien,
SIZE_Tdéclaré dans l’API Windows (accessible via<BaseTsd.h>). Il est défini comme suit:typedef ULONG_PTR SIZE_T;, lui même étant défini commetypedef unsigned __int3264 ULONG_PTR;dontunsigned __int3264est ununsigned intde 32 ou de 64 bits selon la plateforme.Etant spécifique à l’API Windows, il n’est pas portable contrairement à
size_t.
Mettons ces informations en forme dans un tableau que nous allons faire évoluer au fur et à mesure que de nouvelles informations s’ajoutent.
| type | type réel | Portabilité |
|---|---|---|
size_t |
implementation-defined | oui (standard, ISO C et POSIX) |
SIZE_T |
ULONG_PTR | non (API Windows) |
Dépendance à l’ABI et au modèle de données
Ok, le standard reste assez flou sur la taille réelle de size_t.
Sa taille dépend de l’architecture. Sur les systèmes 32 bits, size_t a souvent une taille de 32 bits. Et sur systèmes 64 bits, une taille de 64 bits, mais ce n’est pas garanti.
La taille réelle de size_t dépend de l’ABI (Application Binary Interface) et du modèle de données utilisé par le compilateur:
| Modèle | Système | short |
int |
long |
long long |
size_t |
pointeur |
|---|---|---|---|---|---|---|---|
| IP16 | MS-DOS, CP/M, Unix 16-bit, PDP-11 | 16 bits | 16 bits | 16 bits | 64 bits | 16 bits | 16 bits |
| LP32 | Win16, macOS 68k | 16 bits | 16 bits | 32 bits | 64 bits | 32 bits | 32 bits |
| ILP32 | Win32, Linux 32, BSD | 16 bits | 32 bits | 32 bits | 64 bits | 32 bits | 32 bits |
| LLP64 | Windows 64 (Windows après XP), MinGW | 16 bits | 32 bits ⚠️ | 32 bits | 64 bits | 64 bits | 64 bits |
| LP64 | Linux 64, macOS, Solaris, BSD, Cygwin sur Windows | 16 bits | 32 bits ⚠️ | 64 bits | 64 bits | 64 bits | 64 bits |
| ILP64 | Unix HPC | 16 bits | 64 bits | 64 bits | 64 bits | 64 bits | 64 bits |
| SILP64 | UNICOS de Cray | 64 bits | 64 bits | 64 bits | 64 bits | 64 bits | 64 bits |
(Sources: Wikipedia: 64-bit data models, Doc C++, Doc Rust, Doc Oracle)
⚠️ Les types signed ont la même taille en octets que les types
unsigned.
Surtout, n’utilisez niintniunsigned intpour manipuler des tailles, des quantités ou des index, utilisezsize_t.
ILP64 n’est presque jamais utilisé comme ABI système. Il est utilisé par des bibliothèques scientifiques comme:
Note concernant les compilateurs 16 bits:
Dans l’architecture x86 segmentée, les objets ordinaires étaient généralement limités à 64 Ko par segment, bien que les pointeurs
hugepuissent adresser des objets plus grands, ce qui pouvait théoriquement dépasser la plage représentable par unsize_tde 16 bits.
std::size_t en C++
En C++, le type std::size_t est le type canonique pour représenter toute forme de taille, de quantité ou d’index, dépassant le cadre de la simple manipulation de mémoire.
On y accède par le header <cstddef> (ou tout autre header standard qui l’inclut indirectement, comme <vector> ou <string>).
std::size_tn’est pas réservé aux fonctions de la bibliothèque standard. C’est le type à privilégier dans votre propre code pour toute variable ayant une sémantique de taille ou d’index (taille d’une image, nombre de joueurs, numéro d’un élément dans une liste, etc).
A l’instar du C, le standard C++ n’impose aucun type sous-jacent fixe pour std::size_t ; sa définition exacte est implementation-defined mais sa largeur est d’au moins 16 bits (“The bit width of size_t is not less than 16. (since C++11)”).
Petite information amusante au sujet de
sizeof:
sizeof,sizeof...(C++11) etalignof(C++11) retournent tous unstd::size_t.
std::size_test lui-même défini comme le type retourné parsizeof.Cette définition circulaire délègue la décision finale au compilateur, qui choisit le type le plus adapté à l’architecture.
En plus d’hériter des garanties du C, le C++ apporte ses propres précisions:
- Ses limites sont accessibles via
std::numeric_limits<std::size_t>. - Il est utilisé par convention comme base pour définir le type membre
size_typede tous les conteneurs de la STL.
| Variante | Header | Namespace | |
|---|---|---|---|
size_t |
<stddef.h> |
Global | Héritage du C |
std::size_t |
<cstddef> |
std:: |
Version idiomatique C++ |
Le standard C++ précise que <cstddef> place le type dans le namespace std et peut (mais n’est pas obligé de) le placer également dans le namespace global. Pour un code portable et propre, préférez toujours std::size_t.
Les pièges du typage
Le mélange signé / non-signé
Mélanger des types signés et std::size_t est une source majeure de bugs:
int i = -1;
std::size_t n = 10;
if (i < n)
{
// On s'attend à true, mais ce sera false
}Lorsqu’un type signé et un type non signé sont utilisés dans une opération (ici i < n), C++ applique les usual arithmetic conversions.
Le type signé (int) est converti vers le type non-signé (std::size_t). i = -1 devient une valeur non-signée sur 64 bits, std::size_t{-1}. La valeur -1 devient alors la valeur maximale de std::size_t par overflow (2⁶⁴-1), ce qui est bien supérieur à 10.
C’est d’ailleurs ce que souligne la règle ES.100 des C++ Core Guidelines: “Don’t mix signed and unsigned arithmetic”.
Ca aura donc l’effet suivant:
int i = -1;
std::size_t n = 10;
if (static_cast<std::size_t>(i) < n)
{
// On s'attend à true, mais ce sera false
}Arithmétique différente: Les types non signés (comme
std::size_t) suivent une arithmétique modulo2ⁿ: toute opération dépassant la capacité wrap-around de manière définie. Les types signés, eux, peuvent subir un comportement indéfini (undefined behavior) si le résultat dépasse la plage représentable. Le standard ne garantit rien: le compilateur peut optimiser en supposant que ça n’arrive jamais.
Les promotions intégrales des petits types vers int préservent la valeur et n’introduisent pas d’overflow.
L’underflow dans les boucles
L’utilisation de std::size_t dans les boucles décrémentales est particulièrement risquée:
std::vector<int> v = { ... };
// DANGEREUX: Si v est vide, v.size() - 1 provoque un underflow massif
for (std::size_t i = v.size() - 1; i >= 0; --i) { ... }
Les alternatives signées en C: ssize_t et ptrdiff_t
Le cas ssize_t
std::size_t est le type canonique pour les tailles, les quantités et les index, mais il ne doit pas être utilisé pour représenter une différence ou une distance (qui peuvent être négatives).
Le type ssize_t est un type historique des systèmes POSIX (Linux/Unix). Il est couramment utilisé dans les fonctions système (comme read ou write) pour retourner soit une taille, soit une erreur (via une valeur négative).
| type | type réel | Portabilité |
|---|---|---|
size_t |
implementation-defined | oui (standard, ISO C et POSIX) |
ssize_t |
implementation-defined | non (POSIX uniquement) |
SIZE_T |
ULONG_PTR | non (API Windows) |
SSIZE_T |
ULONG_PTR | non (API Windows) |
ssize_t n’est donc pas un bon candidat pour représenter une différence/distance en C.
ptrdiff_t: Le type des distances
Le type ptrdiff_t est l’alias standard pour un type entier signé représentant le résultat de la soustraction de deux pointeurs (ptr2 - ptr1).
Il peut être formellement défini en C via l’expression suivante:
typedef typeof((int*)nullptr - (int*)nullptr) ptrdiff_t;Comme le laisse penser son nom, c’est sémantiquement son sens premier. Mais il est également utilisé pour représenter une différence entre deux index, tant que celle-ci reste comprise entre PTRDIFF_MIN et PTRDIFF_MAX.
Exemple 1: Restaurer un pointeur après réallocation
1
2
3
4
5
6
7
8
9
10
11
char *buffer = malloc(1024);
char *current = buffer + 512; // Pointeur au milieu du bloc
ptrdiff_t offset = current - buffer; // On mémorise la distance relative
char *reallocatedBuffer = realloc(buffer, 2048);
if (reallocatedBuffer)
{
// Si le bloc a été déplacé en mémoire, 'current' est désormais invalide.
// On utilise l'offset pour rétablir le pointeur à la bonne position.
current = reallocatedBuffer + offset;
}
Exemple 2: Calculer un index à partir d’un pointeur
int values[] = {10, 20, 30, 40, 50};
int *position = &values[3]; // Pointeur vers l'élément '40'
ptrdiff_t index = position - values; // index = 3Limitation de la plage de valeurs
-
size_test garanti de faire au moins 16 bits. -
ptrdiff_test garanti de faire au moins 17 bits avant C23, et au moins 16 bits à partir de C23. Cette différence permet historiquement de garantir que la différence entre deux adresses peut être stockée dans un type signé sans perte de précision (en réservant un bit supplémentaire pour le signe). Ce n’est plus garanti.
Mais concrètement sur les architectures 64 bits modernes, size_t et ptrdiff_t sont définis sur 64 bits.
Il n’est donc pas garanti que tout size_t tienne toujours dans un ptrdiff_t, même si ce n’est pas un problème car ptrdiff_t a les garanties suivantes:
- Il est conçu pour contenir la différence entre deux pointeurs pointant dans le même objet ou tableau.
- Le standard impose que
ptrdiff_tsoit suffisamment grand pour représenter toutes ces différences légales (n’excédant pas les capacités offertes par l’architecture).
Même si size_t peut représenter des valeurs supérieures à PTRDIFF_MAX sur certaines plateformes, aucune différence de pointeurs légale dans le même objet ne pourra dépasser PTRDIFF_MAX. Les différences de pointeurs sont limitées par la taille maximale d’un objet contigu en mémoire.
Les alternatives signées en C++: std::ssize et std::ptrdiff_t
Contrairement à une idée reçue, il n’existe pas de type std::ssize_t dans le standard C++. Le comité C++ a jugé qu’un tel type serait redondant avec std::ptrdiff_t.
Le standard a choisi une fonction plutôt qu’un type. std::ssize() (P1227R2) retourne l’équivalent signé de la taille du conteneur. Plus précisément, son type de retour est: std::common_type_t<std::ptrdiff_t, std::make_signed_t<typename C::size_type>>.
Il est également possible d’obtenir l’équivalent signé d’un type via le trait de type std::make_signed_t:
using signed_size_t = std::make_signed_t<std::size_t>;Ce type signed_size_t peut (par abus de langage) être considéré comme un équivalent à std::ptrdiff_t (bien qu’en pratique ce soit souvent le cas), ce n’est cependant pas garanti. std::ptrdiff_t étant défini par la norme selon une suite d’exigences, ça laisse une marge de manoeuvre aux compilateurs quant à la définition concrète du type sous-jacent.
// std::ssize renvoie un type signé
for (auto i = std::ssize(v) - 1; i >= 0; --i) { ... }
std::ptrdiff_t: Le type des distances
Comme ptrdiff_t en C, le type std::ptrdiff_t est l’alias standard pour un type entier signé en C++. Type adapté pour représenter n’importe quelle soustraction entre deux pointeurs (ptr2 - ptr1).
Il peut être formellement défini via l’expression suivante:
using ptrdiff_t = decltype(static_cast<int*>(nullptr) - static_cast<int*>(nullptr));| type | type réel | Portabilité | Usage sémantique |
|---|---|---|---|
std::size_t |
implementation-defined | oui (standard ISO C++) | Indexes, quantités et tailles d’objets dans un conteneur ou tableau |
std::ptrdiff_t |
implementation-defined | oui (standard ISO C++) | Différences entre deux pointeurs |
Sa largeur est garantie de faire au moins 17 bits (The bit width of std::ptrdiff_t is not less than 17. (since C++11))
Concernant les plages de valeurs de
std::size_tetstd::ptrdiff_t, le standard C++ donne les mêmes garanties que le standard C.
std::size_tn’est pas formellement compris dansstd::ptrdiff_t, mais ce n’est pas un problème pour autant. Nous en avons parlé ici.
std::uintptr_t et std::intptr_t
Bien que std::size_t soit souvent utilisé pour des offset mémoire au sein d’un même objet (comme avec la macro offsetof qui donne un std::size_t), il n’est pas destiné à faire des calculs d’adresses complexes. Pour convertir un pointeur en entier afin d’effectuer de l’arithmétique bas niveau (masquage de bits, calcul d’alignement, etc), préférez std::uintptr_t ou std::intptr_t, qui est garanti d’être assez large pour contenir un pointeur.
| type | type réel | Portabilité | Usage sémantique |
|---|---|---|---|
std::size_t |
implementation-defined | oui (standard ISO C++) | Indexes, quantités et tailles d’objets dans un conteneur ou tableau |
std::ptrdiff_t |
implementation-defined | oui (standard ISO C++) | Différences entre deux pointeurs |
std::uintptr_t |
implementation-defined | oui (standard ISO C++) | Arithmétique sur pointeurs |
std::intptr_t |
implementation-defined | oui (standard ISO C++) | Arithmétique sur pointeurs avec valeurs négatives |
Dans la STL (Standard Template Library)
Les conteneurs de la STL (vector, list, string, etc) définissent des alias internes pour garantir la généricité du code.
Ils sont visibles en public dans les classes, et dans presque toutes les signatures de fonctions membres:
-
T::size_type: Type non signé pour représenter le nombre d’éléments stockés. C’est notamment le type de retour de la méthodestd::vector<T>::size()et le type attendu par l’opérateurstd::vector<T>::operator[]. Ce type est très souventstd::size_tpar défaut. -
T::difference_type: Type signé pour les distances. C’est le type retourné par l’opérateur de soustraction entre deux itérateurs (it2 - it1) ou par la fonctionstd::distance. Ce type est très souventstd::ptrdiff_tpar défaut.
std::vector<int> numbers = {10, 20, 30};
std::vector<int>::size_type size = numbers.size(); // Type de retour de .size()
std::vector<int>::difference_type distance = numbers.end() - numbers.begin(); // Distance entre itérateursstd::vector<int> numbers = {10, 20, 30};
std::size_t size = numbers.size();
std::ptrdiff_t distance = numbers.end() - numbers.begin();Ou si vous avez peur de mal typer vos variables, je vous encourage vivement à utiliser auto:
auto numbers = {10, 20, 30};
auto size = numbers.size();
auto distance = numbers.end() - numbers.begin();Il prend le type retourné par les fonctions, sans risque de conversion maladroite.
Valeur sentinelle de std::string
La fonction std::string::find retourne une valeur de type std::string::size_type. Cet alias correspond à std::allocator_traits<Allocator>::size_type, dont le type réel est systématiquement std::size_t pour l’allocateur par défaut.
Pour être exact,
std::stringest un alias de la classe templatestd::basic_string<CharT, Traits, Allocator>. Cette précision est utile car les types que nous allons manipuler en dépendent.
Cette fonction std::string::find retourne la position de l’élément trouvé:
auto string = std::string{"Hello World!"};
std::size_t position = string.find("World"); // position vaut 5La STL propose une valeur sentinelle pour indiquer que la chaîne recherchée n’a pas été trouvée:
1
2
3
4
5
6
7
auto string = std::string{"Hello World!"};
std::size_t position = string.find("Word"); // position vaut std::string::npos
if (position == std::string::npos)
{
std::println("Chaîne non trouvée");
}
std::string::npos est une constante de type std::size_t. Cette valeur sentinelle vaut -1.
-1 dans un type non-signé ? C’est parfaitement légal: dans l’arithmétique non signée, les overflow/underflow ont la garantie de boucler. -1 devient donc la plus grande valeur possible de std::size_t (qu’on ne peut pas formellement citer car le standard ne garantit pas de largeur exacte pour ce type).
Ce mécanisme, parfois source de bugs, est utilisé ici pour garantir une valeur sentinelle impossible à atteindre pour une taille réelle de chaîne.
Literals (Depuis C++23)
Le C++23 introduit des literals pour manipuler ces types sans conversion implicite:
| Suffixe | Type |
|---|---|
uz, zu (et variantes) |
std::size_t |
z, Z
|
std::make_signed_t<std::size_t> |
// Manipulation d'un index avec le literal uz (C++23) et la fonction std::size() (C++17)
for (auto i = 0uz; i < std::size(container); ++i)
{
// ...
}// Boucle décrémentale sûre, avec le literal signé z (C++23) et la fonction std::size() (C++20)
// i peut devenir négatif (-1), ce qui arrête proprement la boucle
for (auto i = std::ssize(container) - 1; i >= 0z; --i)
{
// ...
}
Le cas particulier de Qt: qsizetype
qsizetype est un type faisant partie du framework Qt. Cette section ne concerne que les développeurs qui l’utilisent.
Le framework Qt a toujours privilégié les types signés (historiquement int) pour ses conteneurs.
Avec l’arrivée du 64 bits, le type int (32 bits) était limité à 2 Go.
qsizetype a été créé (dans Qt 5.10) pour répondre aux mêmes besoins que std::size_t et permettre l’usage de -1 comme valeur sentinelle (QString::indexOf() retourne -1 si non trouvé).
qsizetype permet de monter à 64 bits tout en restant signé.
qsizetype est défini comme étant la version signée de std::size_t:
using qsizetype = QIntegerForSizeof<std::size_t>::Signed;On reconnait QIntegerForSizeof<T>::Signed, l’équivalent made in Qt pour std::make_signed_t<T>, défini comme:
template <int> struct QIntegerForSize;
template <> struct QIntegerForSize<1> { typedef quint8 Unsigned; typedef qint8 Signed; };
template <> struct QIntegerForSize<2> { typedef quint16 Unsigned; typedef qint16 Signed; };
template <> struct QIntegerForSize<4> { typedef quint32 Unsigned; typedef qint32 Signed; };
template <> struct QIntegerForSize<8> { typedef quint64 Unsigned; typedef qint64 Signed; };
#if defined(Q_CC_GNU) && defined(__SIZEOF_INT128__)
template <> struct QIntegerForSize<16> { __extension__ typedef unsigned __int128 Unsigned; __extension__ typedef __int128 Signed; };
#endif
template <class T> struct QIntegerForSizeof: QIntegerForSize<sizeof(T)> { };
using qsizetype = QIntegerForSizeof<std::size_t>::Signed;
La taille de qsizetype est directement définie sur celle de std::size_t (sizeof(std::size_t)).
Si std::size_t fait 32 bits -> qsizetype est un qint32.
Si std::size_t fait 64 bits -> qsizetype est un qint64.
Il s’agit de l’équivalent Qt de std::size_t, mais avec une différence fondamentale: il est signé.
Dans la plupart des plateformes modernes, qsizetype est identique à std::ptrdiff_t, mais cette équivalence n’est pas garantie par le standard. En revanche, qsizetype possède systématiquement la même largeur (nombre de bits) que std::size_t, c’est donc un équivalent à std::make_signed_t<std::size_t>.
Un choix de conception contestable
Le choix de Qt d’utiliser un type signé pour des tailles est souvent critiqué. Bien que cela permette l’utilisation de valeurs sentinelles (comme -1), cela autorise également des états sémantiquement absurdes: rien n’interdit techniquement d’écrire qsizetype n = -5;, ce qui n’a aucun sens pour une mesure de taille physique.
Ce choix de conception introduit une dissonance sémantique permanente dès que l’on sort de l’écosystème de Qt. Le développeur doit jongler entre deux modèles mentaux opposés: l’un où une valeur négative est une erreur légitime (Qt), et l’autre où une taille est par définition une quantité absolue non-signée (la STL et le langage (sizeof)). Cette ambiguïté rend chaque interaction propice aux bugs de signe.
La friction entre Qt et la STL
L’existence de qsizetype crée une “frontière de types” permanente. Puisque Qt a fait le choix du signé pour ses conteneurs alors que la STL et le langage (sizeof) utilisent le non-signé, le développeur se retrouve à devoir arbitrer entre deux mondes incompatibles.
Cela force souvent le développeur à jongler entre trois types pour manipuler des tailles:
-
std::size_t(non-signé standard) -
std::ptrdiff_t(signé standard) -
qsizetype(signé Qt).
Les comparaisons mixtes
Dès que vous comparez un index issu d’une recherche Qt avec une taille ou un index standard, le piège se referme.
Le risque est qu’une valeur “non trouvée” (-1 utilisé comme sentinelle) soit interprétée comme une valeur positive gigantesque lors de la comparaison avec un std::size_t.
1
2
3
4
5
6
7
8
9
10
11
12
QString url = "/api/v1/resource/data"; // Pas de paramètres '?' ici
std::size_t MaxPathLength = 128;
auto queryStart = url.indexOf('?');
// Comparaison entre un qsizetype (signé) et un std::size_t (non-signé)
// Si '?' n'est pas trouvé (queryStart = -1), la condition sera VRAIE car -1 > 128 en non-signé.
if (queryStart > MaxPathLength)
{
// On rejette l'URL car on croit que le chemin est trop long !
return Error::BadRequest;
}
Activez le warning
-Wsign-comparepour être avertis de ce genre de problème, car c’est l’une des sources de bugs les plus fréquentes en C++.
Nous avons détaillé le mécanisme à l’oeuvre ici.
Le langage ayant lui-même choisi std::size_t pour exprimer les tailles physiques (sizeof), cela force à des conversions incessantes, même dans un projet “100% Qt” (et jusque dans l’implémentation même du framework).
Comparaisons sûres (C++20)
Pour résoudre définitivement ce problème sans conversion manuelle risquée, le C++20 a introduit une famille de fonctions dans le header <utility>:
std::cmp_equalstd::cmp_not_equalstd::cmp_lessstd::cmp_less_equalstd::cmp_greaterstd::cmp_greater_equal
Ces fonctions appliquent une logique correcte selon le signe de chaque valeur, empêchant les conversions implicites dangereuses.
QString url = "/api/v1/resource/data"; // Pas de paramètres '?' ici
std::size_t maxPathLength = 128;
// Solution moderne et sûre:
if (std::cmp_greater(queryStart, maxPathLength))
{
// La comparaison est mathématiquement correcte: -1 > 128 est FAUX.
return Error::BadRequest;
}L’asymétrie des conversions
Le passage d’un type à l’autre n’est jamais neutre, car leurs capacités diffèrent.
Sens 1: De Qt vers le standard (qsizetype => std::size_t)
La conversion est techniquement sûre pour toutes les tailles car la plage positive de qsizetype tient toujours dans un std::size_t. Cependant, elle détruit la sémantique d’erreur:
qsizetype qtSize = -1; // En Qt, la sentinelle -1 signifie sémantiquement "non trouvé" ou "erreur"
std::size_t stdSize = qtSize;
// stdSize vaut désormais 18 446 744 073 709 551 615.
// L'erreur est devenue une taille gigantesque "valide"Sens 2: Du standard vers Qt (std::size_t => qsizetype)
C’est ici que le risque d’overflow est le plus critique. Si vous manipulez une donnée dépassant la moitié de la mémoire adressable (ex: un énorme fichier), la conversion produira une valeur négative:
// Imaginons un buffer de 9 exaoctets sur un système très spécifique
std::size_t hugeSize = 9'000'000'000'000'000'000uz;
qsizetype qtSize = hugeSize;
// qtSize devient négatif par overflow
// Qt croira que votre buffer est une erreur ou une chaîne videInteropérabilité des conteneurs
Ces frictions obligent à une vigilance constante lors de l’interaction entre les deux mondes. Tenter de réserver de la place dans une QList en se basant sur la taille d’un std::vector (ou inversement) génère systématiquement un warning.
Par exemple, si nous voulons dimensionner un std::vector par rapport à la taille d’une QList:
std::vector<int> v = { ... };
QList<int> list;
// Warning: conversion de size_t vers qsizetype
// Le compilateur avertit que v.size() pourrait ne pas tenir dans list
list.reserve(v.size()); Frictions avec les appels système et le langage
Les appels système de Qt doivent systématiquement convertir leurs types signés vers les types non-signés attendus par le système (POSIX ou Windows).
Le jonglage entre ces mondes génère un bruit de code permanent, obligeant à choisir son camp et à caster systématiquement.
-
Entrées/Sorties (I/O):
QFile::readouQIODevice::writeprennent unqint64(ouqsizetype), alors que les appels système sous-jacents (read,write) utilisentsize_t(non-signé), impliquant une conversion. -
Manipulation mémoire: Des fonctions comme
QByteArray::fromRawData(const char *data, qsizetype size)demandent unqsizetype, mais les fonctions système de copie (memcpy) appelées en interne attendent unsize_t.
La documentation de Qt montre d’ailleurs souvent cette gymnastique, où un sizeof (non-signé) est passé directement à un paramètre qsizetype (signé), comme dans l’exemple de QByteArray::fromRawData(const char *data, qsizetype size):
static const char mydata[] = {
'\x00', '\x00', '\x03', '\x84', '\x78', '\x9c', '\x3b', '\x76',
'\xec', '\x18', '\xc3', '\x31', '\x0a', '\xf1', '\xcc', '\x99',
...
'\x6d', '\x5b'
};
// sizeof(mydata) est un std::size_t, converti ici implicitement en qsizetype
QByteArray data = QByteArray::fromRawData(mydata, sizeof(mydata));Ceci change deux fois le domaine de signe de la valeur (non-signé -> signé -> non-signé en interne lors de l’appel système).
N’est-ce pas absurde d’imposer un type signé pour des tailles, pour finir par le convertir systématiquement ? Introduisant au passage des risques d’erreurs inutiles (si on passe une valeur négative en argument) ou des coûts supplémentaires si la fonction Qt vérifie systématiquement que la valeur passée n’est pas négative.
Faut-il utiliser qsizetype (Qt) ?
Si vous utilisez Qt, le type qsizetype est un passage obligé, mais il agit comme un corps étranger dès que vous sollicitez les fonctions de la STL ou des fonctions système. L’utilisation de std::ssize() (C++20) est souvent le meilleur moyen de “ramener” les conteneurs STL dans le monde signé de Qt pour éviter les frictions.
QList<int> list = { ... };
std::vector<int> vector = { ... };
// On unifie tout en signé pour éviter les warnings et les bugs de sentinelles
if (std::ssize(list) < std::ssize(vector)) { ... }Si votre code n’est pas fortement lié à Qt, confinez qsizetype aux strictes parties qui l’utilisent. Préférez les standards std::size_t et std::ptrdiff_t.
Mais comme nous l’avons vu, ce n’est pas une question simple. Utiliser qsizetype introduit un grand nombre de frictions avec la STL, le langage et les appels système. Mais ne pas l’utiliser introduit des conversions incessantes entre vos types standards et les types attendus par Qt.
Aucun des deux choix n’est idéal et gratuit (hormis se tourner vers autre chose que Qt ?). A noter que ce n’est pas le seul point de friction. On peut noter aussi le copy-on-write et les itérateurs propres à Qt.
Une 3ème option s’offre à nous, car Qt fait quelques efforts pour se conformer au standard et se rendre compatible avec la STL (bien qu’il reste encore du chemin):
Si votre code est suffisamment générique, que vous utilisez les customization points, auto et les comparaisons sûres, la propagation du type correct sera automatique et ses manipulations seront sûres.
Cette approche permet de prévenir les risques d’erreur tout en déléguant la responsabilité du choix des types à l’appelant. Votre code devient ainsi agnostique et plus résilient.
Les recommandations contradictoires de C++ Core Guidelines
Les C++ Core Guidelines reconnaissent ce conflit historique entre la STL et les besoins de calcul.
Ne mélangez pas signé et non signé (ES.100)
Le principe est simple: Ne mélangez pas l’arithmétique signée et non-signée (Don’t mix signed and unsigned arithmetic). Le mélange provoque des conversions silencieuses et des bugs difficiles à tracer. Nous l’avons illustré avec les frictions de Qt.
Préférez le signé pour les index (ES.107)
Ces guidelines recommandent de préférer les types signés pour les indices de tableaux.
Comme nous l’avons vu avec les boucles décrémentales qui peuvent provoquer un underflow si l’index est non-signé, cette guideline vise à prévenir ce genre d’erreur.
La position ambiguë sur size_t
Les guidelines se retrouvent ici dans une impasse: elles recommandent le signé pour les index (ES.107) tout en devant composer avec std::size_t imposé par la STL pour les tailles de conteneurs et le langage (sizeof).
En effet, les index sont très massivement affectés ou comparés avec des tailles, qui sont non-signées. Causant un nombre considérable d’interactions entre des valeurs signées et non-signées dans un programme. Cette guideline rentre donc complètement en contradiction avec la 1ère (ES.100).
C’est exactement la même dissonance que celle rencontrée avec Qt, montrant que le débat entre signé et non-signé pour les tailles reste l’un des points les plus clivants du C++.
De nombreux développeurs (dont vous aurez deviné, je fais partie) rangent les index et les tailles dans la même arithmétique non-signée (std::size_t). Réservant les index signés uniquement aux boucles décrémentales (en priorisant une autre forme d’écriture pour éviter d’y avoir recours).
Aller plus loin: