HOWTO · C++
Opérateurs de pré-incrémentation VS de post-incrémentation en C++
Cet article montre comment utiliser le pré-incrémentation VS post-incrémentation en C++
Sur cette page
Pour une variable arithmétique intégrée en C++, ++x et x++ ajoutent tous deux un à x. La différence tient à la valeur produite par l’expression : la pré-incrémentation produit la valeur mise à jour, tandis que la post-incrémentation produit la valeur antérieure à la mise à jour. Choisissez selon la valeur dont l’expression englobante a besoin. Pour un type défini par l’utilisateur, les opérateurs d’incrémentation surchargés suivent leur propre définition ; par convention, ils offrent généralement des comportements analogues pour les formes préfixée et postfixée.
Différence entre les opérations de pré-incrémentation et de post-incrémentation en C++
Pour les types arithmétiques intégrés, ++x met x à jour avant de produire le résultat de l’expression. x++ produit l’ancienne valeur et effectue l’incrémentation dans le cadre de l’opération. Dans les deux cas, la variable change une seule fois. Les opérateurs surchargés peuvent avoir des détails différents, même s’ils suivent généralement la même distinction entre préfixe et postfixe.
Exécutez cet exemple :
#include <iostream>
int main() {
int value = 4;
int previous = value++;
int current = ++value;
std::cout << previous << ' ' << current << ' ' << value << '\n';
}
4 6 6
previous reçoit la valeur antérieure à la première incrémentation ; current reçoit la valeur après la deuxième incrémentation, qui est aussi la valeur finale de value.
La position de l’incrémentation dans le corps d’une boucle détermine la valeur affichée par ce corps. Dans la première boucle, i++ est l’expression affichée : elle affiche la valeur actuelle, puis incrémente i. Dans la deuxième, ++j s’exécute avant l’expression d’affichage ; le corps affiche donc la valeur mise à jour.
#include <iostream>
int main() {
int i = 0;
while (i < 5) {
std::cout << i++ << '\n';
}
std::cout << "final i = " << i << '\n';
}
#include <iostream>
int main() {
int j = 0;
while (j < 5) {
std::cout << ++j << '\n';
}
std::cout << "final j = " << j << '\n';
}
La première boucle affiche 0, 1, 2, 3 et 4, puis final i = 5 ; la deuxième affiche 1, 2, 3, 4 et 5, puis final j = 5. Les libellés visibles final i et final j restent en anglais. Dans les deux cas, le compteur commence à 0, le corps s’exécute cinq fois et le compteur finit à 5. Ces exemples effectuent l’incrémentation dans le corps de la boucle. En revanche, lorsque l’incrémentation se trouve dans l’expression d’itération d’une boucle for, elle s’exécute après le corps. Si son résultat est ignoré, remplacer ++k par k++ ne change pas la séquence : les deux boucles for (int k = 0; k < 5; ++k) et la version avec k++ parcourent les valeurs 0, 1, 2, 3 et 4, puis se terminent avec k égal à 5.
Évitez de modifier et de lire la même variable dans une seule expression
Ne combinez pas l’incrémentation d’une variable avec une autre lecture de cette variable dans une même expression non séquencée. Par exemple, k++ * k a un comportement indéfini en C++ : la modification et l’autre lecture ne sont pas séquencées l’une par rapport à l’autre. Son résultat est imprévisible et ne peut pas servir de sortie d’exemple valide.
Si le calcul souhaité consiste à multiplier l’ancienne valeur par la suivante, rendez les étapes explicites et séquencées : initialisez k, enregistrez sa valeur dans old, incrémentez k, puis calculez old * k.
Les instructions distinctes enregistrent l’ancienne valeur avant d’incrémenter k, puis multiplient la valeur enregistrée par la valeur mise à jour. Exécutez cet exemple :
#include <iostream>
int main() {
int k = 10;
int old = k;
++k;
int product = old * k;
std::cout << product << '\n';
}
110
Comment le choix change avec les itérateurs et les types définis par l’utilisateur
La distinction de valeur compte surtout lorsqu’une incrémentation est combinée à une autre opération dans la même expression. Le cas courant est le déréférencement : *it++ signifie *(it++) ; il lit donc l’élément vers lequel pointe l’itérateur, puis avance l’itérateur. À l’inverse, *++it avance d’abord, puis lit le nouvel élément. Sauter ou répéter un élément pendant un parcours relève souvent exactement de cette confusion entre préfixe et postfixe, et non d’une erreur dans la condition de boucle.
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{10, 20, 30};
auto it = values.begin();
std::cout << *it++ << '\n';
std::cout << *it << '\n';
std::cout << *++it << '\n';
}
10
20
30
La première ligne affiche 10 et laisse l’itérateur pointer sur 20 ; l’affichage de *it confirme la nouvelle position avant que *++it n’avance à nouveau et n’affiche 30.
Pour un type défini par l’utilisateur, les deux formes sont deux fonctions différentes. Par convention, la pré-incrémentation est déclarée comme operator++() et renvoie une référence vers l’objet mis à jour, tandis que la post-incrémentation est déclarée comme operator++(int) — cet int est un marqueur fictif, et non une valeur transmise par l’appelant —, conserve une copie de l’ancien état, incrémente via la forme préfixe et renvoie cette copie par valeur.
#include <iostream>
class Counter {
public:
explicit Counter(int value) : value_(value) {}
Counter& operator++() {
++value_;
return *this;
}
Counter operator++(int) {
Counter old = *this;
++(*this);
return old;
}
int value() const { return value_; }
private:
int value_;
};
int main() {
Counter counter{5};
Counter old = counter++;
Counter& ref = ++counter;
std::cout << old.value() << ' ' << counter.value() << ' '
<< ( &ref == &counter ) << '\n';
}
5 7 1
Ici, old conserve la valeur de départ 5, le compteur lui-même se termine à 7 après les deux incrémentations, et le 1 final confirme que la pré-incrémentation a renvoyé le compteur lui-même, et non une copie. La même distinction explique pourquoi le résultat de ++x désigne toujours x lui-même, alors que le résultat de x++ est une copie temporaire de l’ancienne valeur.
C’est aussi là que la question des performances devient concrète. Pour un entier intégré dont le résultat est ignoré, les compilateurs génèrent généralement le même code pour les deux formes ; préférer le préfixe y est donc une convention de lisibilité, et non un gain de vitesse. La copie de la post-incrémentation ne coûte quelque chose que lorsque copier représente un travail réel : un itérateur ou un compteur défini par l’utilisateur qui copie son état, ou un opérateur surchargé qui fait plus qu’ajouter un. Si un type ne définit qu’une seule des deux formes, l’autre est tout simplement indisponible ; vérifiez les déclarations du type au lieu de supposer que les deux existent.
Quand utiliser la pré-incrémentation ou la post-incrémentation
Utilisez la post-incrémentation lorsque l’expression a besoin de l’ancienne valeur, comme dans int previous = value++;. Utilisez la pré-incrémentation lorsque la valeur mise à jour est nécessaire, ou lorsque la valeur de l’expression est ignorée, comme dans l’expression d’itération d’une boucle.
Pour les itérateurs, la pré-incrémentation est une convention courante lorsque l’ancienne valeur de l’itérateur n’est pas nécessaire. Les opérateurs surchargés d’un itérateur défini par l’utilisateur suivent son implémentation, même s’ils reproduisent généralement la distinction intégrée entre préfixe et postfixe. Une opération postfixée sur un itérateur peut conserver et renvoyer une copie de l’itérateur précédent ; la pré-incrémentation peut éviter ce travail pour les types d’itérateurs dont la copie a un coût. Cela ne permet pas d’établir une différence de vitesse mesurable pour un programme donné : les optimisations du compilateur et l’implémentation de l’itérateur influent sur le travail généré.
Conclusion
Les deux formes incrémentent la variable une fois. La pré-incrémentation fournit la valeur mise à jour, tandis que la post-incrémentation fournit la valeur précédente. Choisissez la forme qui exprime clairement la valeur dont l’expression a besoin ; pour les itérateurs, privilégiez la pré-incrémentation lorsque l’ancienne valeur n’est pas utilisée, sans supposer qu’elle garantit un gain de vitesse.
FAQ
Quelle est la principale différence entre la pré-incrémentation et la post-incrémentation ?
Pour les types arithmétiques intégrés, ++x effectue d’abord la mise à jour et fournit la valeur mise à jour. x++ fournit la valeur précédente et effectue l’incrémentation. Les opérateurs surchargés suivent leur propre définition, même s’ils proposent généralement des comportements analogues pour les formes préfixée et postfixée.
Puis-je utiliser l’une ou l’autre forme dans une boucle ?
Oui. Dans l’expression d’itération d’une boucle for, le résultat est généralement ignoré ; remplacer ++i par i++ ne change donc pas la séquence d’itération de cette boucle.
Quelle forme est la plus rapide en C++ ?
Il n’existe pas de classement universel des vitesses pour toutes les expressions et tous les types. La post-incrémentation peut entraîner une copie temporaire pour les itérateurs ou les types définis par l’utilisateur ; l’optimisation peut supprimer le travail inutile. Choisissez en fonction de la valeur nécessaire et de la clarté du code, puis mesurez un cas d’utilisation réel avant d’affirmer qu’une forme est plus performante.
Quand la post-incrémentation est-elle utile ?
Utilisez-la lorsque l’expression a besoin de la valeur précédant l’incrémentation, par exemple pour affecter cette valeur à une autre variable.
La pré-incrémentation et la post-incrémentation se comportent-elles de la même façon dans tous les langages ?
Non. Des opérateurs de forme similaire peuvent obéir à des règles propres à chaque langage. Vérifiez la sémantique du langage utilisé ; les explications présentées ici concernent C++.