(notez que le titre de la question d'origine avait "au lieu d'une rvalue" plutôt que "au lieu d'une référence const". L'une des réponses ci-dessous est en réponse à l'ancien titre. Ce problème a été corrigé pour plus de clarté)

Une construction courante en C et C ++ concerne les affectations chaînées, par exemple

    int j, k;
    j = k = 1;

Le second = est effectué en premier, avec l'expression k=1 ayant pour effet secondaire que k est mis à 1, tandis que la valeur de l'expression elle-même est 1.

Cependant, une construction qui est légale en C ++ (mais pas en C) est la suivante, qui est valide pour tous les types de base:

    int j, k=2;
    (j=k) = 1;

Ici, l'expression j=k a pour effet secondaire de définir j sur 2, et l'expression elle-même devient une référence à j, qui définit ensuite j sur 1. Comme je comprendre, c'est parce que l'expression j=k renvoie un non - const int&, par exemple d'une manière générale, une valeur l.

Cette convention est généralement également recommandée pour les types définis par l'utilisateur, comme expliqué dans «Point 10: faire renvoyer une référence (non-const) à * this» dans Meyers Effective C ++ (mine d'addition entre parenthèses). Cette section du livre n'essaie pas d'expliquer pourquoi la référence est une non - const ou même de noter la non - const ness au passage.

Bien sûr, cela ajoute certainement des fonctionnalités, mais la déclaration (j=k) = 1; semble pour le moins gênante.

Si la convention devait à la place avoir des références de retour d'affectation intégrées const , alors les classes personnalisées utiliseraient également cette convention, et la construction chaînée d'origine autorisée en C fonctionnerait toujours, sans copies ou déplacements superflus. Par exemple, ce qui suit s'exécute correctement:

#include <iostream>
using std::cout;

struct X{
  int k;
  X(int k): k(k){}
  const X& operator=(const X& x){
  // the first const goes against convention
    k = x.k;
    return *this;
  }
};

int main(){
  X x(1), y(2), z(3);
  x = y = z;
  cout << x.k << '\n'; // prints 3
}

L'avantage étant que les 3 (intégrés C, intégrés C ++ et types personnalisés C ++) sont tous cohérents en n'autorisant pas les idiomes comme (j=k) = 1.

L'ajout de cet idiome entre C et C ++ était-il intentionnel? Et si oui, quel type de situation justifierait son utilisation? En d'autres termes, quel avantage non fallacieux cette extension des fonctionnalités offre-t-elle jamais?

3
xdavidliu 10 août 2017 à 21:34

2 réponses

Meilleure réponse

De par leur conception, une différence fondamentale entre C et C ++ est que C est un langage lvalue-discarding et C ++ est un langage lvalue-préservant .

Avant C ++ 98, Bjarne avait ajouté des références au langage afin de rendre possible la surcharge d'opérateurs. Et les références, pour être utiles, exigent que la valeur des expressions soit préservée plutôt que rejetée.

Cette idée de préserver la valeur n'a pas été vraiment formalisée avant C ++ 98. Dans les discussions qui ont précédé le standard C ++ 98, le fait que les références exigeaient que la valeur d'une expression soit préservée a été noté et formalisé et c'est à ce moment que C ++ a fait une rupture majeure et utile de C et est devenu un langage préservant les valeurs.

C ++ s'efforce de préserver la «valeur» de tout résultat d'expression aussi longtemps que possible. Il s'applique à tous les opérateurs intégrés et également à l'opérateur d'affectation intégré. Bien sûr, il n'est pas fait pour permettre l'écriture d'expressions comme (a = b) = c, car leur comportement serait indéfini (au moins sous le standard C ++ d'origine). Mais à cause de cette propriété de C ++, vous pouvez écrire du code comme

int a, b = 42;
int *p = &(a = b);

L'utilité est une autre question, mais encore une fois, ce n'est qu'une des conséquences de la conception préservant les valeurs des expressions C ++.

Quant à savoir pourquoi ce n'est pas une const lvalue ... Franchement, je ne vois pas pourquoi cela devrait l'être. Comme tout autre opérateur intégré préservant les valeurs en C ++, il préserve simplement le type qui lui est donné.

4
AnT 16 mars 2019 à 16:48

Je répondrai à la question dans le titre.

Supposons qu'il renvoie une référence rvalue. Il ne serait pas possible de renvoyer une référence à un objet nouvellement assigné de cette façon (car c'est une lvalue). S'il n'est pas possible de renvoyer une référence à un objet nouvellement attribué, il faut créer une copie. Ce serait terriblement inefficace pour les objets lourds, par exemple les conteneurs.

Prenons un exemple de classe similaire à std :: vector.

Avec le type de retour actuel, l'affectation fonctionne de cette façon (je n'utilise pas de modèles et d'idiome de copie et d'échange délibérément pour garder le code aussi simple que possible):

class vector {
     vector& operator=(const vector& other) {
         // Do some heavy internal copying here.
         // No copy here: I just effectively return this.
         return *this;
     }
};

Supposons qu'il retourne une rvalue:

class vector {
     vector operator=(const vector& other) {
          // Do some heavy stuff here to update this. 
          // A copy must happen here again.
          return *this;
      }
};

Vous pourriez penser à renvoyer une référence rvalue, mais cela ne fonctionnerait pas non plus: vous ne pouvez pas simplement déplacer *this (sinon, une chaîne d'affectations a = b = c s'exécuterait b), donc une deuxième copie sera également nécessaire pour le retourner.

La question dans le corps de votre message est différente: renvoyer un const vector& est en effet possible sans aucune des complications ci-dessus, donc cela ressemble plus à une convention pour moi.

Remarque: le titre de la question fait référence aux éléments intégrés, tandis que ma réponse couvre les classes personnalisées. Je pense que c'est une question de cohérence. Il serait assez surprenant qu'il agisse différemment pour les types intégrés et personnalisés.

1
kraskevich 10 août 2017 à 19:26