You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -130,21 +130,25 @@ Mélanger des types signés et ``std::size_t`` est une source majeure de bugs:
130
130
int i = -1;
131
131
std::size_t n = 10;
132
132
133
-
if (i < n) {
134
-
// On s'attend à 'true', mais ce sera 'false' !
133
+
if (i < n)
134
+
{
135
+
// On s'attend à true, mais ce sera false
135
136
}
136
137
{% endhighlight %}
137
138
138
139
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**](https://en.cppreference.com/w/cpp/language/usual_arithmetic_conversions.html).<br>
139
140
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.
140
141
142
+
> C'est d'ailleurs ce que souligne la règle [**ES.100**](#ne-mélangez-pas-signé-et-non-signé-es100) des C++ Core Guidelines: **"Don't mix signed and unsigned arithmetic"**.
143
+
141
144
Ca aura donc l'effet suivant:
142
145
{% highlight cpp highlight_lines="4" %}
143
146
int i = -1;
144
147
std::size_t n = 10;
145
148
146
-
if (static_cast<std::size_t>(i) < n) {
147
-
// On s'attend à 'true', mais ce sera 'false' !
149
+
if (static_cast<std::size_t>(i) < n)
150
+
{
151
+
// On s'attend à true, mais ce sera false
148
152
}
149
153
{% endhighlight %}
150
154
@@ -361,17 +365,14 @@ Si ``std::size_t`` fait 32 bits -> ``qsizetype`` est un ``qint32``.<br>
361
365
Si ``std::size_t`` fait 64 bits -> ``qsizetype`` est un ``qint64``.
362
366
363
367
Il s'agit de l'équivalent Qt de ``std::size_t``, mais avec une différence fondamentale: **il est signé**.
364
-
C'est donc un équivalent à [``std::make_signed_t<std::size_t>``](#les-alternatives-signées-en-c-stdssize-et-stdptrdiff_t), c'est à dire **équivalent à [``std::ptrdiff_t``](#stdptrdiff_t-le-type-des-distances)**.
365
-
366
-
Il bénéficie donc par dépendance au type ``std::size_t`` des mêmes garanties que ``std::ptrdiff_t`` (supporte la même largeur que ``std::ptrdiff_t``).
367
368
368
-
Mais l'arrivée de ``qsizetype``vient également avec une **série de problèmes** dont on aimerait bien se passer.
369
+
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>``](#les-alternatives-signées-en-c-stdssize-et-stdptrdiff_t).
369
370
370
-
Face à ces **équivalences**, on pourrait utiliser ``qsizetype`` et ``std::ptrdiff_t``de manière interchangeable (en décidant d'**ignorer les coûts** liés aux conversions si les types sous-jacents ne sont pas strictement identiques, ce qui en soi **est déjà un problème**).
371
+
### Un choix de conception contestable
371
372
372
-
Mais ``qsizetype`` introduit un grand nombre de **frictions**avec la **STL**, le **langage** et les **appels système**.
373
+
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.
373
374
374
-
Pour vous aider à y voir plus clair, détaillons ces points de friction pour **comprendre les coûts** que ça entraine et **prévenir les risques d'erreurs**:
375
+
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.
375
376
376
377
### La friction entre Qt et la STL
377
378
@@ -382,34 +383,99 @@ Cela force souvent le développeur à jongler entre trois types pour manipuler d
382
383
-``std::ptrdiff_t`` (signé standard)
383
384
-``qsizetype`` (signé Qt).
384
385
385
-
#### L'enfer des comparaisons mixtes
386
+
#### Les comparaisons mixtes
386
387
387
388
Dès que vous comparez un index issu d'une recherche Qt avec une taille ou un index standard, le piège se referme.
388
389
389
-
Le risque est **réel**: une valeur négative de ``qsizetype``(``-1`` utilisé comme **sentinelle** si non trouvé) [sera interprétée comme une valeur positive gigantesque](#le-mélange-signé--non-signé) lors de la comparaison avec un ``std::size_t``.
390
+
Le risque est qu'une valeur "non trouvée" (``-1`` utilisé comme **sentinelle**) [soit interprétée comme une valeur positive gigantesque](#le-mélange-signé--non-signé) lors de la comparaison avec un ``std::size_t``.
390
391
391
-
{% highlight cpp linenos highlight_lines="9" %}
392
-
const auto text = "Hello World!"; // de type const char[13]
393
-
const auto string = QString{text};
392
+
{% highlight cpp linenos highlight_lines="8" %}
393
+
QString url = "/api/v1/resource/data"; // Pas de paramètres '?' ici
394
+
std::size_t MaxPathLength = 128;
394
395
395
-
// indexOf retourne -1 si le mot-clé n'est pas trouvé
396
-
auto position = string.indexOf("Word");
396
+
auto queryStart = url.indexOf('?');
397
397
398
-
// Comparaison d'une valeur signée (qsizetype) avec une non-signée (std::size_t)
399
-
// Si "Word" n'est pas trouvé (position = -1), la condition sera VRAIE car -1 > 13 en non-signé.
400
-
if (position > std::size(text))
398
+
// Comparaison entre un qsizetype (signé) et un std::size_t (non-signé)
399
+
// Si '?' n'est pas trouvé (queryStart = -1), la condition sera VRAIE car -1 > 128 en non-signé.
400
+
if (queryStart > MaxPathLength)
401
401
{
402
-
// ...
402
+
// On rejette l'URL car on croit que le chemin est trop long !
403
+
return Error::BadRequest;
403
404
}
404
405
{% endhighlight %}
405
406
406
-
> **Activez** le warning ``-Wsign-compare`` pour être avertis de ce genre de problème.
407
+
> **Activez** le warning ``-Wsign-compare`` pour être avertis de ce genre de problème, car c'est l'une des sources de bugs les plus fréquentes en C++.
407
408
{: .block-warning }
408
409
409
410
Nous avons détaillé le mécanisme à l'oeuvre [ici](#le-mélange-signé--non-signé).
410
411
411
412
**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**).
412
413
414
+
#### Comparaisons sûres (C++20)
415
+
416
+
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>``:
Ces fonctions appliquent une logique correcte selon le signe de chaque valeur, **empêchant les conversions implicites dangereuses**.
425
+
426
+
{% highlight cpp %}
427
+
QString url = "/api/v1/resource/data"; // Pas de paramètres '?' ici
428
+
std::size_t maxPathLength = 128;
429
+
430
+
// Solution moderne et sûre:
431
+
if (std::cmp_greater(queryStart, maxPathLength))
432
+
{
433
+
// La comparaison est mathématiquement correcte: -1 > 128 est FAUX.
434
+
return Error::BadRequest;
435
+
}
436
+
{% endhighlight %}
437
+
438
+
#### L'asymétrie des conversions
439
+
440
+
Le passage d'un type à l'autre n'est jamais neutre, car leurs capacités diffèrent.
441
+
442
+
**Sens 1: De Qt vers le standard (``qsizetype`` => ``std::size_t``)**
443
+
444
+
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**:
445
+
{% highlight cpp %}
446
+
qsizetype qtSize = -1; // En Qt, la sentinelle -1 signifie sémantiquement "non trouvé" ou "erreur"
// L'erreur est devenue une taille gigantesque "valide"
451
+
{% endhighlight %}
452
+
453
+
**Sens 2: Du standard vers Qt (``std::size_t`` => ``qsizetype``)**
454
+
455
+
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:
456
+
{% highlight cpp %}
457
+
// Imaginons un buffer de 9 exaoctets sur un système très spécifique
// Qt croira que votre buffer est une erreur ou une chaîne vide
463
+
{% endhighlight %}
464
+
465
+
#### Interopérabilité des conteneurs
466
+
467
+
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.
468
+
469
+
Par exemple, si nous voulons dimensionner un ``std::vector`` par rapport à la taille d'une ``QList``:
470
+
{% highlight cpp %}
471
+
std::vector<int> v = { ... };
472
+
QList<int> list;
473
+
474
+
// Warning: conversion de size_t vers qsizetype
475
+
// Le compilateur avertit que v.size() pourrait ne pas tenir dans list
476
+
list.reserve(v.size());
477
+
{% endhighlight %}
478
+
413
479
#### Frictions avec les appels système et le langage
414
480
415
481
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).
@@ -420,9 +486,7 @@ Le jonglage entre ces mondes génère un bruit de code permanent, obligeant à *
420
486
421
487
-**Manipulation mémoire**: Des fonctions comme [``QByteArray::fromRawData(const char *data, qsizetype size)``](https://doc.qt.io/qt-6/qbytearray.html#fromRawData) demandent un ``qsizetype``, mais les fonctions système de copie ([``memcpy``](https://man7.org/linux/man-pages/man3/memcpy.3.html)) appelées en interne attendent un ``size_t``.
422
488
423
-
La documentation de Qt montre d'ailleurs souvent cette gymnastique, où un ``sizeof`` (non-signé) est passé directement à un paramètre ``qsizetype`` (signé).
424
-
425
-
Par exemple ici, dans l'exemple donné par la documentation Qt sur l'utilisation de [``QByteArray::fromRawData(const char *data, qsizetype size)``](https://doc.qt.io/qt-6/qbytearray.html#fromRawData):
489
+
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)``](https://doc.qt.io/qt-6/qbytearray.html#fromRawData):
@@ -439,7 +503,33 @@ Ceci change **deux fois** le domaine de signe de la valeur (non-signé -> signé
439
503
440
504
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.
441
505
442
-
**En résumé:** 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)](https://en.cppreference.com/w/cpp/iterator/size.html) est souvent le meilleur moyen de "ramener" les conteneurs STL dans le monde signé de Qt pour éviter les frictions.
506
+
## Les recommandations contradictoires de C++ Core Guidelines
507
+
508
+
Les [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) reconnaissent ce conflit historique entre la STL et les besoins de calcul.
509
+
510
+
### Ne mélangez pas signé et non signé (ES.100)
511
+
512
+
Le principe est simple: [**Don't mix signed and unsigned arithmetic**](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#res-mix). Le mélange provoque des **conversions silencieuses** et des bugs difficiles à tracer. Nous l'avons illustré avec les [frictions de Qt](#les-comparaisons-mixtes).
513
+
514
+
### Préférez le signé pour les index (ES.107)
515
+
516
+
Ces guidelines recommandent de [**préférer les types signés pour les indices de tableaux**](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es107-dont-use-unsigned-for-subscripts-prefer-gslindex).
517
+
518
+
Comme nous l'avons vu avec [les boucles décrémentales](#lunderflow-dans-les-boucles) qui peuvent provoquer un **underflow** si l'index est non-signé, cette guideline vise à prévenir ce genre d'erreur.
519
+
520
+
### La position ambiguë sur ``size_t``
521
+
522
+
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``).
523
+
524
+
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](#ne-mélangez-pas-signé-et-non-signé-es100)).
525
+
526
+
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++**.
527
+
528
+
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).
529
+
530
+
### Faut-il utiliser ``qsizetype`` ?
531
+
532
+
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)](https://en.cppreference.com/w/cpp/iterator/size.html) est souvent le meilleur moyen de "ramener" les conteneurs STL dans le monde signé de Qt pour éviter les frictions.
if (std::ssize(list) < std::ssize(vector)) { ... }
450
540
{% endhighlight %}
451
541
452
-
### Faut-il utiliser ``qsizetype``
453
-
454
-
-**Lorsque vous manipulez des objets Qt**, utiliser les types attendus et retournés par Qt (comme ``qsizetype``) permet d'être sûr de ne pas avoir de conversions. Mais vous serez [parfois obligés de les faire interagir avec des ``std::size_t``](#lenfer-des-comparaisons-mixtes).
455
-
456
-
- Si votre code n'est **pas fortement lié à Qt**, il est cohérent de confiner la propagation ``qsizetype`` aux strictes parties qui l'utilisent. Et d'utiliser les standards ``std::size_t`` et ``std::ptrdiff_t`` dans le reste de votre projet lorsque vous avez une **sémantique** de **taille**, de **quantité**, d'**index**, de **différence** ou de **distance**.
542
+
**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``.
457
543
458
-
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**. Et ne pas l'utiliser introduit des conversions (**risques et coût**) entre les types qu'on manipule ``std::size_t``/``std::ptrdiff_t``et le type attendu par les fonctions Qt ``qsizetype``.
544
+
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.
459
545
460
-
> **Aucun** des deux choix **n'est idéal et gratuit** (hormis se tourner vers **autre chose que Qt** ?<br>
461
-
> A noter que ce n'est **pas le seul point de friction** entre Qt, la STL et le langage. On peut noter aussi le [*copy-on-write*](https://en.wikipedia.org/wiki/Copy-on-write) et les [iterateurs](https://wiki.qt.io/Iterators)).
546
+
> **Aucun** des deux choix **n'est idéal et gratuit** (hormis se tourner vers **autre chose que Qt** ?).
547
+
> A noter que ce n'est **pas le seul point de friction**. On peut noter aussi le [*copy-on-write*](https://en.wikipedia.org/wiki/Copy-on-write) et les [itérateurs](https://wiki.qt.io/Iterators) propres à Qt.
462
548
{: .block-warning }
463
549
464
-
Une **3ème option** s'offre à nous, car **Qt fait quelques efforts** pour **se conformer au standard** et **se rendre compatible** avec la STL (mais il reste encore beaucoup de chemin):
550
+
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):
465
551
466
-
> Si votre **code est [suffisamment générique](/articles/c++/programmation_generique)**, que vous utilisez les [**customization point**](/articles/c++/customization_point_design) et [**auto**](/articles/c++/auto), la **propagation** de ``qsizetype`` ou de ``std::size_t`` sera **automatique**. Et il sera à vous de bien [utiliser ``std::ssize()``](#frictions-avec-les-appels-système-et-le-langage) lorsque les deux types risquent d'entrer en collision.<br>
467
-
> Ceci permettant de **prévenir les risques d'erreur** tout en **délèguant** les questions de **coûts** (liées aux conversions) **à l'appelant** de vos fonctions. Ainsi, votre code deviendrait **agnostique** des types utilisés, **laissant à l'appelant la responsabilité de ces choix**.
552
+
> Si votre **code est [suffisamment générique](/articles/c++/programmation_generique)**, que vous utilisez les [**customization points**](/articles/c++/customization_point_design), [**auto**](/articles/c++/auto) et les [**comparaisons sûres**](#comparaisons-sûres-c20), la **propagation** du type correct sera **automatique** et ses manipulations seront **sûres**.
553
+
>
554
+
> 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.
0 commit comments