48 votes

Est-il légal d'utiliser l'opérateur d'incrément dans un appel de fonction C ?

Il y a eu un débat en cours dans cette question de savoir si le code suivant est légal C++:

std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
    bool isActive = (*i)->update();
    if (!isActive)
    {
        items.erase(i++);  // *** Is this undefined behavior? ***
    }
    else
    {
        other_code_involving(*i);
        ++i;
    }
}

Le problème ici est que, erase() invalidera l'itérateur en question. Si cela se produit avant i++ est évaluée, puis incrémentation i comme cela est techniquement un comportement indéfini, même si elle semble travailler avec un compilateur. Un côté du débat, dit que tous les arguments de la fonction sont complètement évaluées avant que la fonction est appelée. De l'autre côté, dit, "les seules garanties sont que i++ arrivera avant la prochaine instruction et après i++ est utilisé. Si c'est avant l'effacement(i++) est invoquée ou par la suite par le compilateur, dépendantes".

J'ai ouvert cette question, nous l'espérons, pour trancher ce débat.

64voto

Michael Kristofik Points 16035

Dit-le C++ standard 1.9.16:

Lors de l'appel d'une fonction (ou non la fonction est en ligne), tous les la valeur de calcul et d'effets secondaires associé avec n'importe quel argument l'expression, ou avec postfix expression désignant l'appelé la fonction, est séquencée avant de l'exécution de toute expression ou déclaration dans le corps de l'appelé fonction. (Note: les calculs de la Valeur et les effets secondaires associés à l' l'argument différent expressions sont séquencé.)

Donc, il me semble que ce code:

foo(i++);

est parfaitement légal. Elle s'incrémente i puis appelez foo avec la précédente valeur de i. Toutefois, ce code:

foo(i++, i++);

les rendements comportement indéfini parce que le paragraphe 1.9.16 dit aussi:

Si un effet indésirable sur un scalaire objet est non par rapport à un autre effet secondaire sur le même objet scalaire ou une valeur de calcul à l'aide de la valeur de la même scalaire de l'objet, de la le comportement est indéfini.

14voto

Bill the Lizard Points 147311

Pour construire sur de Kristo réponse,

foo(i++, i++);

les rendements comportement indéfini parce que l'ordre des arguments de la fonction sont évalués n'est pas défini (et dans le cas plus général, car si vous lisez une variable deux fois dans une expression où vous l'écris aussi, le résultat n'est pas défini). Vous ne savez pas quel argument va être incrémenté d'abord.

int i = 1;
foo(i++, i++);

pourrait résulter en un appel de fonction de

foo(2, 1);

ou

foo(1, 2);

ou même

foo(1, 1);

Exécutez les opérations suivantes pour voir ce qui se passe sur votre plate-forme:

#include <iostream>

using namespace std;

void foo(int a, int b)
{
    cout << "a: " << a << endl;
    cout << "b: " << b << endl;
}

int main()
{
    int i = 1;
    foo(i++, i++);
}

Sur ma machine je obtenir

$ ./a.out
a: 2
b: 1

à chaque fois, mais ce code n'est pas portable, donc je m'attends à voir des résultats différents avec différents compilateurs.

5voto

Pete Kirkham Points 32484

La norme dit que l'effet secondaire se produit avant l'appel, de sorte que le code est le même que:

std::list<item*>::iterator i_before = i;

i = i_before + 1;

items.erase(i_before);

plutôt que d'être:

std::list<item*>::iterator i_before = i;

items.erase(i);

i = i_before + 1;

Donc, il est sûr dans ce cas, car la liste.effacer() plus précisément ne pas invalider les itérateurs autre que celui effacées.

Cela dit, c'est mauvais style - la fonction d'effacement de tous les conteneurs renvoie l'itérateur suivant précisément de sorte que vous n'avez pas à vous soucier de l'invalidation d'itérateurs en raison de la réaffectation, de sorte que le idiomatiques code:

i = items.erase(i);

sera sans danger pour les listes, et sera également sans danger pour les vecteurs, les deques et tout autre conteneur de séquence si vous souhaitez modifier votre espace de stockage.

Vous aussi ne pas obtenir le code d'origine pour compiler sans avertissements - vous devriez écrire

(void)items.erase(i++);

pour éviter un avertissement à propos d'un retour non utilisé, ce qui serait un gros indice que vous êtes en train de faire quelque chose de bizarre.

3voto

Himadri Choudhury Points 5300

C'est parfait. La valeur passée serait la valeur de «i» avant l'augmentation.

1voto

jalf Points 142628

Pour construire sur MarkusQ réponse: ;)

Ou plutôt, le projet de Loi de commentaire à lui:

(Edit: Oh, le commentaire est reparti... Oh bien)

Ils sont autorisés à être évalué en parallèle. Si oui ou non il se passe dans la pratique est techniquement hors de propos.

Vous n'avez pas besoin de thread parallélisme pour ce faire cependant, l'évaluation de la première étape à la fois (prendre la valeur de i) avant la seconde (incrémenter i). Parfaitement légal, et certains compilateurs peuvent considérer qu'il est plus efficace que l'évaluation d'un i++ avant de commencer le deuxième.

En fait, j'avais peut s'attendre à une optimisation de la commune. Regardez à partir d'une planification d'instructions point de vue. Vous avez la suite vous avez besoin d'évaluer:

  1. Prendre la valeur de i pour le bon argument
  2. Incrément je dans le bon argument
  3. Prendre la valeur de i pour la gauche argument
  4. Incrémenter i de la gauche argument

Mais il n'y a vraiment pas de dépendance entre la gauche et la droite argument. L'Argument de l'évaluation qui se passe dans un ordre non spécifié, et n'a pas besoin d'être fait de manière séquentielle, soit (et c'est pourquoi new() dans les arguments de la fonction est généralement une fuite de mémoire, même quand enveloppé dans un pointeur intelligent) C'est undefined aussi ce qui se passe lorsque vous modifiez la même variable deux fois dans la même expression. Nous avons une dépendance entre 1 et 2, cependant, et entre 3 et 4. Alors pourquoi le compilateur attendre 2 à terminer avant le calcul des 3? Qui introduit un temps de latence supplémentaire, et ça va prendre plus de temps que nécessaire avant le 4 devient disponible. En supposant qu'il a 1 cycle de latence entre chaque, ça va prendre 3 cycles de 1 est complète jusqu'à ce que le résultat de 4 est prêt et nous pouvons appeler la fonction.

Mais si nous réorganiser et de les évaluer dans l'ordre 1, 3, 2, 4, nous pouvons le faire en 2 cycles. 1 et 3 peuvent être démarrés dans le même cycle (ou même fusionnées en une seule instruction, puisque c'est la même expression), et dans la suite, 2 et 4 peuvent être évalués. Tous les modernes, le PROCESSEUR peut exécuter 3-4 instructions par cycle, et un bon compilateur doit essayer de les exploiter.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X