L'arme secrète du débogueur : ce que les programmeurs C++ négligent
\n\nAvez-vous déjà écrit quelque chose comme i = i++ + ++i; et obtenu des résultats différents selon les compilateurs ? Ou attrapé un bug étrange qui ne se reproduisait que sur le serveur, mais pas sur votre ordinateur portable ? Faites connaissance avec : undefined behavior (comportement indéfini) — l'un des ennemis les plus sournois du développeur C++. Et au centre de ce chaos se trouvent les points de séquence (sequence points).
Dans cet article, nous allons voir ce que sont les sequence points, comment ils sont liés à l'ordre d'évaluation en C++, et — surtout — comment éviter de marcher sur ce râteau. Le contenu est basé sur une question culte de Stack Overflow qui a recueilli des milliers de vues. C'est parti !
\n\nQue sont les points de séquence (sequence points) ?
\n\nImaginez que vous êtes le chef d'orchestre. Chaque musicien (instruction du processeur) doit entrer à un moment précis. Un point de séquence est un moment où tous les « effets secondaires » précédents (modifications de variables, appels de fonctions) sont déjà garantis comme terminés, et les suivants n'ont pas encore commencé.
\n\nEn C++, la norme 98/03 utilisait le terme sequence point. En C++11, il a été remplacé par les relations plus strictes « sequenced before / sequenced after », mais le principe reste le même : entre deux points de séquence, le compilateur a parfaitement le droit de réorganiser les opérations, si cela ne change pas le résultat (selon la règle « as-if »).
\n\nEn termes simples : si vous essayez de modifier la même variable deux fois entre deux points de séquence — vous obtenez un undefined behavior. Le compilateur peut faire n'importe quoi : d'un résultat correct au formatage de votre disque dur (blague, mais presque vrai).
\n\nExemples classiques de points de séquence
\n- \n
- Le point-virgule
;— fin d'une expression \n - La virgule
,en tant qu'opérateur (mais pas dans les arguments d'une fonction !) \n - La fin de chaque expression dans
for(après l'initialisation, la vérification et l'incrémentation) \n - Les opérateurs logiques
&&et||\n - L'opérateur ternaire
?:\n - L'appel de fonction (avant que le corps de la fonction ne commence à s'exécuter) \n
Comment les points de séquence engendrent-ils un comportement indéfini ?
\n\nVoici un exemple classique que tout développeur C++ devrait connaître :
\n\nint i = 0;\ni = i++ + ++i; // UB ! Comportement indéfini\n\nPourquoi est-ce UB ? Parce qu'entre deux points de séquence (dans ce cas, entre le début et la fin de l'expression), la variable i est modifiée deux fois :
- \n
- L'incrémentation postfixée
i++\n - L'incrémentation préfixée
++i\n - L'affectation
i = ...\n
Le compilateur ne sait pas dans quel ordre effectuer ces opérations. Le résultat ? Un compilateur donnera 2, un autre 3, un troisième 0. Et en mode optimisation, il pourrait même supprimer ce code.
\n\nUn autre cas piège : les arguments d'une fonction
\n\nint foo(int a, int b) { return a + b; }\n\nint x = 5;\nint result = foo(++x, ++x); // UB !\n\nIci, la virgule entre les arguments est pas un opérateur virgule, mais un séparateur. Il n'y a pas de point de séquence entre l'évaluation des arguments ++x ! Les deux incrémentations sont effectuées « en parallèle » (du point de vue de la norme), et nous obtenons à nouveau un undefined behavior.
Exemples pratiques : à quoi ressemble UB dans le code réel
\n\nExemple 1 : Bug non évident avec l'indexation
\n\nint arr[5] = {0, 10, 20, 30, 40};\nint idx = 1;\narr[idx] = idx++; // UB !\n\nÀ première vue, tout est simple : on veut écrire 1 dans arr[1], puis incrémenter idx. Mais la norme ne garantit pas l'ordre d'évaluation de l'index idx et de la partie droite idx++. Le résultat ? Cela peut s'écrire dans arr[1], ou dans arr[2].