Question

Pourquoi ++i est-il une valeur l et i++ non ?

Était-ce utile?

La solution

Eh bien, comme un autre intervenant a déjà souligné la raison pour laquelle ++i est une lvalue est de la transmettre à une référence.

int v = 0;
int const & rcv = ++v; // would work if ++v is an rvalue too
int & rv = ++v; // would not work if ++v is an rvalue

La raison de la deuxième règle est de permettre d'initialiser une référence à l'aide d'un littéral, lorsque la référence est une référence à const :

void taking_refc(int const& v);
taking_refc(10); // valid, 10 is an rvalue though!

Pourquoi introduisons-nous une rvalue, vous demandez-vous peut-être.Eh bien, ces termes apparaissent lors de la construction des règles linguistiques pour ces deux situations :

  • Nous voulons avoir une valeur de localisateur.Cela représentera un emplacement qui contient une valeur pouvant être lue.
  • Nous voulons représenter la valeur d'une expression.

Les deux points ci-dessus sont tirés de la norme C99 qui inclut cette jolie note de bas de page très utile :

[ Le nom ''lvalue'' vient à l’origine à partir de l’expression d’affectation E1 = E2, dans lequel l’opérande de gauche E1 est doit être une valeur l (modifiable).Il vaut peut-être mieux le considérer comme représentant un ''localisateur d’objet valeur''.Ce que l’on appelle parfois ''rvalue'' est dans cette Standard décrit comme la ''valeur de une expression''.]

La valeur du localisateur est appelée lvaleur, tandis que la valeur résultant de l'évaluation de cet emplacement est appelée rvaleur.C'est vrai également selon la norme C++ (en parlant de la conversion lvalue en rvalue) :

4.1/2 :La valeur contenue dans l’objet indiquée par la valeur l est la valeur r résultat.

Conclusion

En utilisant la sémantique ci-dessus, il est clair maintenant pourquoi i++ n'est pas une lvalue mais une rvalue.Parce que l'expression renvoyée ne se trouve pas dans i plus (c'est incrémenté !), c'est juste la valeur qui peut être intéressante.Modifier cette valeur renvoyée par i++ cela n'aurait aucun sens, car nous n'avons pas d'emplacement à partir duquel nous pourrions relire cette valeur.Et donc la norme dit qu'il s'agit d'une rvalue, et qu'elle ne peut donc se lier qu'à une référence à const.

Cependant, en revanche, l’expression renvoyée par ++i est l'emplacement (lvalue) de i.Provoquer une conversion lvalue en rvalue, comme dans int a = ++i; en lira la valeur.Alternativement, nous pouvons y faire référence et lire la valeur plus tard : int &a = ++i;.

Notez également les autres occasions où des rvalues ​​sont générées.Par exemple, tous les temporaires sont des rvalues, le résultat de + et moins binaires/unaires et toutes les expressions de valeur de retour qui ne sont pas des références.Toutes ces expressions ne sont pas situées dans un objet nommé, mais portent uniquement des valeurs.Ces valeurs peuvent bien entendu être sauvegardées par des objets qui ne sont pas constants.

La prochaine version C++ inclura ce qu'on appelle rvalue references cela, même s'ils pointent vers nonconst, peuvent se lier à une rvalue.La logique est de pouvoir "voler" les ressources de ces objets anonymes et d'éviter que les copies ne le fassent.En supposant un type de classe qui a surchargé le préfixe ++ (renvoyant Object&) et postfix ++ (renvoyant Object), ce qui suit provoquerait d'abord une copie, et dans le second cas, cela volerait les ressources de la rvalue :

Object o1(++a); // lvalue => can't steal. It will deep copy.
Object o2(a++); // rvalue => steal resources (like just swapping pointers)

Autres conseils

D'autres personnes ont abordé la différence fonctionnelle entre l 'accroissement post et pré.

En ce qui étant un est concerné lvalue , ne peut pas être i++ attribué parce qu'il ne se réfère pas à une variable. Il fait référence à une valeur calculée.

En ce qui concerne la cession, les deux éléments suivants n'a pas de sens dans le même genre de façon:

i++   = 5;
i + 0 = 5;

Parce que pré-incrément renvoie une référence à la variable incrémentée plutôt qu'une copie temporaire, est une lvalue ++i.

Préférer pré-incrément pour des raisons de performance devient une idée particulièrement bonne lorsque vous incrémentez quelque chose comme un objet iterator (par exemple dans le STL) qui pourrait bien être un bon peu plus lourd qu'un int.

Il semble que beaucoup de gens expliquent comment ++i est une lvalue, mais pas le pourquoi , comme dans pourquoi ne le comité de normalisation C ++ mettre ce en vedette, en particulier à la lumière du fait que C ne permet pas non plus comme lvalues. De cette discussion sur comp.std.c ++ , il apparaît qu'il est si vous pouvez prendre son adresse ou céder à une référence. Un exemple de code extrait du poste de Christian Bau:

   int i;
   extern void f (int* p);
   extern void g (int& p);

   f (&++i);   /* Would be illegal C, but C programmers
                  havent missed this feature */
   g (++i);    /* C++ programmers would like this to be legal */
   g (i++);    /* Not legal C++, and it would be difficult to
                  give this meaningful semantics */

Par ailleurs, si se trouve être i un type intégré, puis les instructions d'affectation telles que le comportement Invoke ++i = 10 undefined , car deux fois modifiés est <=> entre les points de séquence.

Je reçois l'erreur de lvalue lorsque je tente de compiler

i++ = 2;

mais pas quand je change à

++i = 2;

En effet, l'opérateur préfixe (++ i) change la valeur i, puis i, il retourne peut encore être affecté. L'opérateur de Postfix (i ++) change la valeur i, mais retourne une copie temporaire de l'ancien valeur , qui ne peut être modifié par l'opérateur d'affectation.


Réponse à la question initiale :

Si vous parlez avec les opérateurs d'incrémentation dans une déclaration par eux-mêmes, comme dans une boucle, il fait vraiment aucune différence. Préincrémentation semble être plus efficace, parce que lui-même doit post-incrémentation incrémenter et retourner une valeur temporaire, mais un compilateur optimisera loin cette différence.

for(int i=0; i<limit; i++)
...

est le même que

for(int i=0; i<limit; ++i)
...

Les choses deviennent un peu plus compliqué lorsque vous utilisez la valeur de retour de l'opération dans le cadre d'une déclaration plus grande.

Même les deux déclarations simples

int i = 0;
int a = i++;

et

int i = 0;
int a = ++i;

sont différents. Ce qui incrémente l'opérateur que vous choisissez d'utiliser comme une partie des états multi-opérateurs dépend de ce que le comportement est destiné. En bref, pas vous ne pouvez pas choisir un. Vous devez comprendre les deux.

incrément POD Pre:

La pré-incrément doit agir comme si l'objet a été augmentée avant l'expression et être utilisable dans cette expression comme si cela se produisait. Ainsi les normes C ++ Comitee a décidé qu'il peut également être utilisé comme valeur l.

POD incrément message:

Le post-incrément devrait augmenter l'objet POD et retourner une copie pour une utilisation dans l'expression (Voir n2521 section 5.2.6). En tant que copie n'est pas réellement une variable qui en fait une l valeur ne fait aucun sens.

Objets:

incrément avant et après sur les objets est juste du sucre syntaxique de la langue constitue un moyen d'appeler des méthodes sur l'objet. Ainsi, sur le plan technique Les objets ne sont pas limités par le comportement standard de la langue, mais seulement par les restrictions imposées par les appels de méthode.

Il est à l'implémenteur de ces méthodes pour rendre le comportement de ces objets reflètent le comportement des objets POD (Il est nécessaire, mais pas prévu).

Objets pré-incrément:

L'exigence (comportement attendu) ici est que les objets est incrémentée (ce qui signifie dépendant objet) et la méthode renvoient une valeur qui est modifiable et ressemble à l'objet d'origine après l'incrément est arrivé (comme si l'augmentation était arrivé avant cette déclaration).

Pour ce faire est siple et seulement exiger que la méthode renvoie une référence à elle-même. Une référence est une valeur L et donc se comportera comme prévu.

Objets de post-incrémentation:

L'exigence (comportement attendu) ici est que l'objet est incrémenté (de la même manière que pré-incrément) et la valeur renvoyée ressemble à la valeur ancienne et est non-mutables (de sorte qu'il ne se comporte pas comme un l -valeur).

Non-Mutable:
Pour ce faire, vous devez renvoyer un objet. Si l'objet est utilisé dans une expression, il sera construit une copie dans une variable temporaire. Les variables temporaires sont const et donc il sera non mutables et se comportent comme prévu.

On dirait l'ancienne valeur:
Ceci est simplement réalisé en créant une copie de l'original (en utilisant probablement le constructeur de copie) avant toute modification makeing. La copie doit être une copie complète sinon toute modification apportée à l'original sur la copie et donc l'état changeront en relation avec l'expression à l'aide de l'objet.

De la même manière que pré-incrément.
Il est probablement préférable de mettre en œuvre augmentation post en termes de pré-incrémentation de sorte que vous obtenez le même comportement

class Node // Simple Example
{
     /*
      * Pre-Increment:
      * To make the result non-mutable return an object
      */
     Node operator++(int)
     {
         Node result(*this);   // Make a copy
         operator++();         // Define Post increment in terms of Pre-Increment

         return result;        // return the copy (which looks like the original)
     }

     /*
      * Post-Increment:
      * To make the result an l-value return a reference to this object
      */
     Node& operator++()
     {
         /*
          * Update the state appropriatetly */
         return *this;
     }
};

Concernant la valeur L

  • Dans C (et Perl par exemple), ni l'un ni l'autre ++i ni i++ sont des LValeurs.

  • Dans C++, i++ n'est pas et LValue mais ++i est.

    ++i est équivalent à i += 1, ce qui équivaut à i = i + 1.
    Le résultat est que nous avons toujours affaire au même objet i.
    Il peut être considéré comme :

    int i = 0;
    ++i = 3;  
    // is understood as
    i = i + 1;  // i now equals 1
    i = 3;
    

    i++ d’autre part, pourrait être considéré comme :
    Nous utilisons d'abord le valeur de i, puis incrémentez le objet i.

    int i = 0;
    i++ = 3;  
    // would be understood as 
    0 = 3  // Wrong!
    i = i + 1;
    

(modifier:mis à jour après une première tentative bâclée).

La principale différence est que i ++ renvoie la valeur de pré-incrémentation alors que ++ i renvoie la valeur de post-incrémentation. J'utiliser normalement ++ i moins d'avoir une très convaincante raison d'utiliser i ++ -. À savoir si je vraiment ne ont besoin de la valeur pré-incrément

à mon humble avis, il est conseillé d'utiliser le formulaire « ++ i ». Bien que la différence entre incrémentation avant et après est pas vraiment mesurable lorsque vous comparez des entiers ou d'autres PODs, l'objet supplémentaire copie que vous devez faire et revenir lorsque vous utilisez « i ++ » peut représenter un impact significatif de la performance si l'objet est soit assez cher de copier ou incrémentée fréquemment.

Par ailleurs - évitez d'utiliser des opérateurs d'incrémentation multiples sur la même variable dans la même déclaration. Vous entrez dans un désordre de « où sont les points de séquence » et non définie pour des opérations, au moins en C. Je pense que certains qui a été nettoyé en Java e C #.

Peut-être que cela a quelque chose à voir avec la façon dont le post-incrément est mis en œuvre. Peut-être est quelque chose comme ceci:

  • Créer une copie de la valeur d'origine dans la mémoire
  • incrémenter la variable d'origine
  • Retour la copie

Étant donné que la copie est ni une variable ni une référence à la mémoire allouée dynamiquement, il ne peut pas être une valeur l.

Comment le compilateur traduit-il cette expression ? a++

Nous savons que nous voulons rendre le non incrémenté version de a, l'ancienne version d'un avant l'incrément.Nous souhaitons également augmenter a comme effet secondaire.En d'autres termes, nous renvoyons l'ancienne version de a, ce qui ne représente plus l'état actuel de a, ce n'est plus la variable elle-même.

La valeur renvoyée est une copie de a qui est placé dans un registre.Ensuite, la variable est incrémentée.Donc ici, vous ne renvoyez pas la variable elle-même, mais vous renvoyez une copie qui est un séparé entité!Cette copie est temporairement stockée dans un registre puis elle est restituée.Rappelez-vous qu'une lvalue en C++ est un objet qui a un emplacement identifiable en mémoire.Mais la copie est stockée à l'intérieur un registre dans le CPU, pas en mémoire. Toutes les valeurs sont des objets qui n'ont pas d'emplacement identifiable en mémoire.Cela explique pourquoi la copie de l'ancienne version de a est une rvalue, car elle est temporairement stockée dans un registre.En général, toutes les copies, valeurs temporaires ou résultats d'expressions longues comme (5 + a) * b sont stockés dans des registres, puis affectés à la variable, qui est une lvalue.

L'opérateur postfix doit stocker la valeur d'origine dans un registre afin de pouvoir renvoyer la valeur non incrémentée comme résultat.Considérez le code suivant :

for (int i = 0; i != 5; i++) {...}

Cette boucle for compte jusqu'à cinq, mais i++ est la partie la plus intéressante.Il s'agit en fait de deux instructions en 1.Nous devons d’abord déplacer l’ancienne valeur de i dans le registre, puis on incrémente i.En code pseudo-assembleur :

mov i, eax
inc i

eax Le registre contient désormais l'ancienne version de i comme copie.Si la variable i réside dans la mémoire principale, cela peut prendre beaucoup de temps au processeur pour récupérer la copie depuis la mémoire principale et la déplacer dans le registre.C'est généralement très rapide pour les systèmes informatiques modernes, mais si votre boucle for itère cent mille fois, toutes ces opérations supplémentaires commencent à s'additionner !Ce serait une pénalité de performance importante.

Les compilateurs modernes sont généralement suffisamment intelligents pour optimiser ce travail supplémentaire pour les types entiers et pointeurs.Pour les types d’itérateurs plus compliqués, ou peut-être les types de classes, ce travail supplémentaire pourrait potentiellement être plus coûteux.

Qu'en est-il de l'incrément de préfixe ++a?

Nous voulons rendre le incrémenté version de a, la nouvelle version de a après l'incrément.La nouvelle version de a représente l'état actuel de a, car c'est la variable elle-même.

D'abord a est incrémenté.Puisque nous voulons obtenir la version mise à jour de a, pourquoi ne pas simplement retourner le variable a lui-même?Nous n'avons pas besoin de faire une copie temporaire dans le registre pour générer une rvalue.Cela nécessiterait un travail supplémentaire inutile.Nous renvoyons donc simplement la variable elle-même sous forme de lvalue.

Si nous n'avons pas besoin de la valeur non incrémentée, il n'est pas nécessaire de faire le travail supplémentaire consistant à copier l'ancienne version de a dans un registre, ce qui est effectué par l'opérateur postfix.C'est pourquoi vous ne devez utiliser que a++ si tu vraiment besoin de renvoyer la valeur non incrémentée.Pour tout autre usage, utilisez simplement ++a.En utilisant habituellement les versions de préfixe, nous n'avons pas à nous soucier de savoir si la différence de performances est importante.

Un autre avantage d'utiliser ++a est qu'il exprime plus directement l'intention du programme :Je veux juste augmenter a!Cependant, quand je vois a++ dans le code de quelqu'un d'autre, je me demande pourquoi veulent-ils renvoyer l'ancienne valeur ?Pourquoi est-ce?

C #:

public void test(int n)
{
  Console.WriteLine(n++);
  Console.WriteLine(++n);
}

/* Output:
n
n+2
*/
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top