Comment se fait-il qu'une référence non const ne puisse pas se lier à un objet temporaire?

226

Pourquoi n'est-il pas autorisé à obtenir une référence non constante à un objet temporaire, quelle fonction getx()renvoie? De toute évidence, cela est interdit par la norme C ++, mais je m'intéresse à l'objectif d'une telle restriction, et non à une référence à la norme.

struct X
{
    X& ref() { return *this; }
};

X getx() { return X();}

void g(X & x) {}    

int f()
{
    const X& x = getx(); // OK
    X& x = getx(); // error
    X& x = getx().ref(); // OK
    g(getx()); //error
    g(getx().ref()); //OK
    return 0;
}
  1. Il est clair que la durée de vie de l'objet ne peut pas être la cause, car une référence constante à un objet est pas interdite par la norme C ++.
  2. Il est clair que l'objet temporaire n'est pas constant dans l'exemple ci-dessus, car les appels à des fonctions non constantes sont autorisés. Par exemple,ref() pourrait modifier l'objet temporaire.
  3. En outre, ref() vous permet de tromper le compilateur et d'obtenir un lien vers cet objet temporaire et cela résout notre problème.

En outre:

Ils disent "assigner un objet temporaire à la référence const prolonge la durée de vie de cet objet" et "Cependant, rien n'est dit sur les références non const". Ma question supplémentaire . L'affectation suivante prolonge-t-elle la durée de vie de l'objet temporaire?

X& x = getx().ref(); // OK
Alexey Malistov
la source
4
Je ne suis pas d'accord avec la partie "la durée de vie de l'objet ne peut pas être la cause", simplement parce qu'il est indiqué dans la norme, que l'attribution d'un objet temporaire à la référence const prolonge la durée de vie de cet objet à la durée de vie de la référence const. Cependant, rien n'est dit sur les références non const ...
SadSido
3
Eh bien, quelle est la cause de ce "Rien n'est dit sur les références non const si ...". Cela fait partie de ma question. Y a-t-il un sens à cela? Peut-être que les auteurs de Standard ont juste oublié les références non const et nous verrons bientôt le prochain problème principal?
Alexey Malistov
2
GotW # 88: Un candidat pour le "Const le plus important". herbsutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry
fnieto - Fernando Nieto
4
@Michael: VC lie les valeurs r aux références non const. Ils appellent cela une fonctionnalité, mais c'est vraiment un bug. (Notez que ce n'est pas un bug parce qu'il est intrinsèquement illogique, mais parce qu'il a été exclu explicitement pour éviter les erreurs idiotes.)
sbi

Réponses:

97

À partir de cet article de blog Visual C ++ sur les références rvalue :

... C ++ ne veut pas que vous modifiiez accidentellement des temporaires, mais appeler directement une fonction membre non const sur une valeur r modifiable est explicite, donc c'est autorisé ...

Fondamentalement, vous ne devriez pas essayer de modifier les temporaires pour la simple raison qu'ils sont des objets temporaires et mourront à tout moment. La raison pour laquelle vous êtes autorisé à appeler des méthodes non const est que, bien, vous êtes invités à faire des choses "stupides" tant que vous savez ce que vous faites et que vous êtes explicite à ce sujet (comme, en utilisant reinterpret_cast). Mais si vous liez un temporaire à une référence non constante, vous pouvez continuer à le faire circuler "pour toujours" juste pour que votre manipulation de l'objet disparaisse, car quelque part en cours de route, vous avez complètement oublié que c'était un temporaire.

Si j'étais vous, je repenserais la conception de mes fonctions. Pourquoi g () accepte-t-il une référence, modifie-t-il le paramètre? Si non, faites-le const référence, si oui, pourquoi essayez-vous de lui passer un temporaire, ne vous souciez-vous pas que ce soit un temporaire que vous modifiez? Pourquoi getx () est-il de retour temporaire de toute façon? Si vous partagez avec nous votre scénario réel et ce que vous essayez d'accomplir, vous pouvez obtenir de bonnes suggestions sur la façon de le faire.

Aller à l'encontre du langage et tromper le compilateur résout rarement les problèmes - généralement cela crée des problèmes.


Edit: Répondre aux questions dans le commentaire: 1) X& x = getx().ref(); // OK when will x die?- Je ne sais pas et je m'en fiche, car c'est exactement ce que je veux dire par "aller à l'encontre de la langue". Le langage dit que "les temporaires meurent à la fin de la déclaration, à moins qu'ils ne soient obligés de const const, auquel cas ils meurent lorsque la référence sort du cadre". En appliquant cette règle, il semble que x soit déjà mort au début de l'instruction suivante, car il n'est pas lié à la référence const (le compilateur ne sait pas ce que ref () renvoie). Ce n'est qu'une supposition cependant.

2) J'ai clairement énoncé le but: vous n'êtes pas autorisé à modifier les temporaires, car cela n'a tout simplement pas de sens (en ignorant les références de valeur C ++ 0x). La question "alors pourquoi suis-je autorisé à appeler des membres non const?" est une bonne réponse, mais je n'ai pas de meilleure réponse que celle que j'ai déjà indiquée ci-dessus.

3) Eh bien, si j'ai raison à propos de x en X& x = getx().ref();mourant à la fin de la déclaration, les problèmes sont évidents.

Quoi qu'il en soit, d'après votre question et vos commentaires, je ne pense pas que même ces réponses supplémentaires vous satisferont. Voici une dernière tentative / résumé: le comité C ++ a décidé qu'il n'était pas logique de modifier les temporaires, par conséquent, ils ont interdit la liaison à des références non const. Il peut y avoir une implémentation du compilateur ou des problèmes historiques ont également été impliqués, je ne sais pas. Ensuite, un cas spécifique a émergé, et il a été décidé que contre toute attente, ils permettraient toujours une modification directe en appelant la méthode non-const. Mais c'est une exception - vous n'êtes généralement pas autorisé à modifier les temporaires. Oui, C ++ est souvent aussi bizarre.

sbk
la source
2
@sbk: 1) En fait, la phrase appropriée est: "... à la fin de l' expression complète ...". Une "expression complète", je crois, est définie comme une expression qui n'est pas une sous-expression d'une autre expression. Que ce soit toujours la même chose que "la fin de la déclaration", je ne suis pas sûr.
sbi
5
@sbk: 2) En fait, vous êtes autorisé à modifier les valeurs r (temporaires). Il est interdit intégré (types , intetc.), mais il est permis pour les types définis par l' utilisateur: (std::string("A")+"B").append("C").
sbi
6
@sbk: 3) La raison pour laquelle Stroustrup donne (en D&E) pour interdire la liaison de rvalues ​​à des références non const est que, si Alexey g()modifiait l'objet (que vous attendez d'une fonction prenant une référence non const), cela modifierait un objet qui va mourir, donc personne ne pourrait de toute façon obtenir la valeur modifiée. Il dit que c'est très probablement une erreur.
sbi
2
@sbk: Je suis désolé si je vous ai offensé, mais je ne pense pas que 2) soit taquin. Les valeurs R ne sont tout simplement pas const, sauf si vous les définissez ainsi et que vous pouvez les modifier, sauf si elles sont intégrées. Il m'a fallu un certain temps pour comprendre si, avec, par exemple, l'exemple de chaîne, je fais quelque chose de mal (JFTR: je ne le fais pas), j'ai donc tendance à prendre cette distinction au sérieux.
sbi
5
Je suis d'accord avec sbi - cette question n'est pas du tout tatillonne. C'est toute la base de la sémantique des mouvements que les valeurs de type de classe sont mieux conservées non const.
Johannes Schaub - litb
38

Dans votre code getx()retourne un objet temporaire, un soi-disant "rvalue". Vous pouvez copier des valeurs r dans des objets (alias. Variables) ou les lier à des références const (ce qui prolongera leur durée de vie jusqu'à la fin de la vie de la référence). Vous ne pouvez pas lier rvalues ​​à des références non const.

Il s'agissait d'une décision de conception délibérée afin d'empêcher les utilisateurs de modifier accidentellement un objet qui va mourir à la fin de l'expression:

g(getx()); // g() would modify an object without anyone being able to observe

Si vous voulez faire cela, vous devrez d'abord faire une copie locale ou de l'objet ou le lier à une référence const:

X x1 = getx();
const X& x2 = getx(); // extend lifetime of temporary to lifetime of const reference

g(x1); // fine
g(x2); // can't bind a const reference to a non-const reference

Notez que la prochaine norme C ++ inclura des références rvalue. Ce que vous appelez des références devient donc des «références de valeur». Vous serez autorisé à lier rvalues ​​aux références rvalue et vous pouvez surcharger les fonctions sur "rvalue-ness":

void g(X&);   // #1, takes an ordinary (lvalue) reference
void g(X&&);  // #2, takes an rvalue reference

X x; 
g(x);      // calls #1
g(getx()); // calls #2
g(X());    // calls #2, too

L'idée derrière les références rvalue est que, puisque ces objets vont mourir de toute façon, vous pouvez profiter de cette connaissance et implémenter ce qu'on appelle la "sémantique de déplacement", un certain type d'optimisation:

class X {
  X(X&& rhs)
    : pimpl( rhs.pimpl ) // steal rhs' data...
  {
    rhs.pimpl = NULL; // ...and leave it empty, but deconstructible
  }

  data* pimpl; // you would use a smart ptr, of course
};


X x(getx()); // x will steal the rvalue's data, leaving the temporary object empty
sbi
la source
2
Salut, c'est une réponse géniale. Besoin de savoir une chose, le g(getx())ne fonctionne pas car sa signature est g(X& x)et get(x)renvoie un objet temporaire, donc nous ne pouvons pas lier un objet temporaire ( rvalue ) à une référence non constante, n'est -ce pas? Et dans votre premier morceau de code, je pense que ce sera const X& x2 = getx();au lieu de const X& x1 = getx();..
SexyBeast
1
Merci d'avoir signalé ce bug dans ma réponse 5 ans après l'avoir écrit! :-/Oui, votre raisonnement est correct, quoique un peu en arrière: nous ne pouvons pas lier les temporaires à des constréférences non (lvalue), et par conséquent le temporaire retourné par getx()(et non get(x)) ne peut pas être lié à la référence lvalue à laquelle l'argument fait référence g().
sbi
Umm, qu'entendez-vous par getx()(et non get(x)) ?
SexyBeast
Quand j'écris "... getx () (et pas get (x)) ..." , je veux dire que le nom de la fonction est getx(), et non get(x)(comme vous l'avez écrit).
sbi
Cette réponse confond la terminologie. Une rvalue est une catégorie d'expression. Un objet temporaire est un objet. Une valeur r peut ou non désigner un objet temporaire; et un objet temporaire peut ou non être désigné par une valeur r.
MM
16

Ce que vous montrez, c'est que le chaînage d'opérateurs est autorisé.

 X& x = getx().ref(); // OK

L'expression est 'getx (). Ref ();' et ceci est exécuté jusqu'à son terme avant d'être affecté à «x».

Notez que getx () ne renvoie pas une référence mais un objet entièrement formé dans le contexte local. L'objet est temporaire mais il n'est pas const, vous permettant ainsi d'appeler d'autres méthodes pour calculer une valeur ou provoquer d'autres effets secondaires.

// It would allow things like this.
getPipeline().procInstr(1).procInstr(2).procInstr(3);

// or more commonly
std::cout << getManiplator() << 5;

Regardez à la fin de cette réponse pour un meilleur exemple de cela

Vous ne pouvez pas lier un temporaire à une référence, car cela générera une référence à un objet qui sera détruit à la fin de l'expression, vous laissant ainsi une référence pendante (qui est désordonnée et la norme n'aime pas désordonnée).

La valeur retournée par ref () est une référence valide mais la méthode ne prête aucune attention à la durée de vie de l'objet qu'elle retourne (car elle ne peut pas avoir cette information dans son contexte). Vous venez de faire l'équivalent de:

x& = const_cast<x&>(getX());

La raison pour laquelle il est correct de le faire avec une référence const à un objet temporaire est que la norme étend la durée de vie du temporaire à la durée de vie de la référence afin que la durée de vie des objets temporaires soit prolongée au-delà de la fin de l'instruction.

La seule question qui reste à résoudre est donc la suivante: pourquoi la norme ne veut-elle pas autoriser une référence aux temporels pour prolonger la durée de vie de l'objet au-delà de la fin de l'énoncé?

Je pense que c'est parce que cela rendrait le compilateur très difficile à corriger pour les objets temporaires. Cela a été fait pour les références const aux temporaires car cela a une utilisation limitée et vous a donc forcé à faire une copie de l'objet pour faire quoi que ce soit d'utile mais fournit des fonctionnalités limitées.

Pensez à cette situation:

int getI() { return 5;}
int x& = getI();

x++; // Note x is an alias to a variable. What variable are you updating.

Prolonger la durée de vie de cet objet temporaire va être très déroutant.
Alors que ce qui suit:

int const& y = getI();

Vous donnera un code intuitif à utiliser et à comprendre.

Si vous souhaitez modifier la valeur, vous devez renvoyer la valeur à une variable. Si vous essayez d'éviter le coût de la recopie de l'objet à partir de la fonction (car il semble que l'objet soit recopié (techniquement)). Alors ne vous embêtez pas, le compilateur est très bon en «optimisation de la valeur de retour»

Martin York
la source
3
"Donc, la seule question qui reste est pourquoi la norme ne veut-elle pas autoriser la référence aux temporels pour prolonger la vie de l'objet au-delà de la fin de la déclaration?" C'est ça! Vous comprenez ma question. Mais je ne suis pas d'accord avec votre opinion. Vous dites "rendre le compilateur très difficile" mais cela a été fait pour référence const. Vous dites dans votre exemple "Remarque x est un alias pour une variable. Quelle variable mettez-vous à jour." Aucun problème. Il existe une variable unique (temporaire). Certains objets temporaires (égal à 5) doivent être modifiés.
Alexey Malistov
@Martin: Les références pendantes ne sont pas seulement désordonnées. Ils pourraient entraîner de graves bogues lors d'un accès ultérieur à la méthode!
mmmmmmmm
1
@Alexey: Notez que le fait de le lier à une référence const améliore la durée de vie d'un temporaire est une exception qui a été ajoutée délibérément (TTBOMK afin de permettre des optimisations manuelles). Aucune exception n'a été ajoutée pour les références non const, car la liaison d'un temporaire à une référence non const a été considérée comme une erreur de programmeur.
sbi
1
@alexy: Une référence à une variable invisible! Pas si intuitif.
Martin York
const_cast<x&>(getX());n'a pas de sens
curiousguy
10

Pourquoi est discuté dans la FAQ C ++ (mine en gras ):

En C ++, les références non const peuvent se lier à lvalues ​​et les références const peuvent se lier à lvalues ​​ou rvalues, mais rien ne peut se lier à une rvalue non const. C'est pour protéger les gens contre la modification des valeurs des temporaires qui sont détruits avant que leur nouvelle valeur puisse être utilisée . Par exemple:

void incr(int& a) { ++a; }
int i = 0;
incr(i);    // i becomes 1
incr(0);    // error: 0 is not an lvalue

Si cette incr (0) était autorisée, soit une valeur temporaire que personne n'a jamais vue serait incrémentée ou - bien pire - la valeur de 0 deviendrait 1. Ce dernier sonne idiot, mais il y avait en fait un bogue comme celui des premiers compilateurs Fortran qui définissait à côté d'un emplacement mémoire pour conserver la valeur 0.

Tony Delroy
la source
Cela aurait été amusant de voir le visage du programmeur qui a été mordu par ce "bug zéro" de Fortran! x * 0 donne x? Quoi? Quoi??
John D
Ce dernier argument est particulièrement faible. Aucun compilateur digne de mention ne changerait jamais la valeur de 0 à 1, ni même interpréterait incr(0);de cette manière. De toute évidence, si cela était autorisé, il serait interprété comme créant un entier temporaire et le transmettant àincr()
user3204459
6

Le principal problème est que

g(getx()); //error

est une erreur logique: gmodifie le résultat de getx()mais vous n'avez aucune chance d'examiner l'objet modifié. S'il gn'avait pas besoin de modifier son paramètre, il n'aurait pas eu besoin d'une référence lvalue, il aurait pu prendre le paramètre par valeur ou par référence const.

const X& x = getx(); // OK

est valide car vous devez parfois réutiliser le résultat d'une expression, et il est assez clair que vous avez affaire à un objet temporaire.

Cependant il n'est pas possible de faire

X& x = getx(); // error

valide sans g(getx())valider, ce que les concepteurs de langage essayaient d'éviter en premier lieu.

g(getx().ref()); //OK

est valide car les méthodes ne connaissent que la constance de la this, elles ne savent pas si elles sont appelées sur une lvalue ou sur une rvalue.

Comme toujours en C ++, vous avez une solution de contournement pour cette règle mais vous devez signaler au compilateur que vous savez ce que vous faites en étant explicite:

g(const_cast<x&>(getX()));
Dan Berindei
la source
5

Il semble que la question initiale de savoir pourquoi cela n'est pas autorisé a reçu une réponse claire: "car il s'agit très probablement d'une erreur".

FWIW, je pensais montrer comment faire, même si je ne pense pas que ce soit une bonne technique.

La raison pour laquelle je veux parfois passer un temporaire à une méthode prenant une référence non constante est de jeter intentionnellement une valeur renvoyée par référence que la méthode appelante ne se soucie pas. Quelque chose comme ça:

// Assuming: void Person::GetNameAndAddr(std::string &name, std::string &addr);
string name;
person.GetNameAndAddr(name, string()); // don't care about addr

Comme expliqué dans les réponses précédentes, cela ne se compile pas. Mais cela compile et fonctionne correctement (avec mon compilateur):

person.GetNameAndAddr(name,
    const_cast<string &>(static_cast<const string &>(string())));

Cela montre simplement que vous pouvez utiliser la conversion pour mentir au compilateur. Évidemment, il serait beaucoup plus propre de déclarer et de passer une variable automatique inutilisée:

string name;
string unused;
person.GetNameAndAddr(name, unused); // don't care about addr

Cette technique introduit une variable locale inutile dans la portée de la méthode. Si, pour une raison quelconque, vous souhaitez empêcher son utilisation plus tard dans la méthode, par exemple pour éviter toute confusion ou erreur, vous pouvez le masquer dans un bloc local:

string name;
{
    string unused;
    person.GetNameAndAddr(name, unused); // don't care about addr
}

- Chris

Chris Pearson
la source
4

Pourquoi voudriez-vous jamais X& x = getx();? Il suffit d'utiliser X x = getx();et de s'appuyer sur RVO.

DevFred
la source
3
Parce que je veux appeler g(getx())plutôt queg(getx().ref())
Alexey Malistov
4
@Alexey, ce n'est pas une vraie raison. Si vous faites cela, vous avez alors une erreur logique, car cela gva modifier quelque chose sur lequel vous ne pouvez plus mettre la main.
Johannes Schaub - litb
3
@ JohannesSchaub-litb peut-être qu'il s'en fiche.
curiousguy
" s'appuyer sur RVO " sauf qu'il ne s'appelle pas "RVO".
curiousguy
2
@curiousguy: C'est un terme très accepté pour cela. Il n'y a absolument rien de mal à l'appeler "RVO".
Puppy
4

La solution de contournement diabolique implique le mot-clé «mutable». En fait, le mal est laissé comme un exercice pour le lecteur. Ou voir ici: http://www.ddj.com/cpp/184403758

Paul
la source
3

Excellente question, et voici ma tentative d'une réponse plus concise (car beaucoup d'informations utiles sont dans les commentaires et difficiles à creuser dans le bruit.)

Toute référence liée directement à un temporaire prolongera sa durée de vie [12.2.5]. En revanche, une référence initialisée avec une autre référence ne le sera pas (même si c'est finalement la même temporaire). Cela a du sens (le compilateur ne sait pas à quoi se réfère finalement cette référence).

Mais cette idée est extrêmement déroutante. Par exemple const X &x = X();, le temporaire durera aussi longtemps que la xréférence, mais const X &x = X().ref();ne le sera PAS (qui sait ce qui a ref()réellement été renvoyé). Dans ce dernier cas, le destructeur de for Xest appelé à la fin de cette ligne. (Ceci est observable avec un destructeur non trivial.)

Cela semble donc généralement déroutant et dangereux (pourquoi compliquer les règles sur la durée de vie des objets?), Mais il y avait probablement un besoin au moins pour les références const, donc la norme définit ce comportement pour eux.

[From sbi comment]: Notez que le fait de le lier à une référence const améliore la durée de vie d'un temporaire est une exception qui a été ajoutée délibérément (TTBOMK afin de permettre des optimisations manuelles). Aucune exception n'a été ajoutée pour les références non const, car la liaison d'un temporaire à une référence non const a été considérée comme une erreur de programmeur.

Tous les temporels persistent jusqu'à la fin de la pleine expression. Pour les utiliser, cependant, vous avez besoin d'une astuce comme vous l'avez fait avec ref(). C'est légal. Il ne semble pas y avoir de bonne raison pour que le cerceau supplémentaire saute, sauf pour rappeler au programmeur qu'il se passe quelque chose d'inhabituel (à savoir, un paramètre de référence dont les modifications seront rapidement perdues).

[Un autre commentaire sbi ] La raison pour laquelle Stroustrup donne (en D&E) pour interdire la liaison de rvalues ​​à des références non const est que, si le g () d'Alexey modifierait l'objet (que vous attendez d'une fonction prenant un non-const référence), cela modifierait un objet qui va mourir, donc personne ne pourrait de toute façon obtenir la valeur modifiée. Il dit que c'est très probablement une erreur.

DS.
la source
2

"Il est clair que l'objet temporaire n'est pas constant dans l'exemple ci-dessus, car les appels à des fonctions non constantes sont autorisés. Par exemple, ref () pourrait modifier l'objet temporaire."

Dans votre exemple, getX () ne renvoie pas de const X, vous pouvez donc appeler ref () de la même manière que vous pourriez appeler X (). Ref (). Vous renvoyez une référence non constante et vous pouvez donc appeler des méthodes non const, ce que vous ne pouvez pas faire est d'affecter la référence à une référence non constante.

Avec le commentaire de SadSidos, cela rend vos trois points incorrects.

Patrick
la source
1

J'ai un scénario que je voudrais partager où j'aimerais pouvoir faire ce que demande Alexey. Dans un plugin Maya C ++, je dois faire le shenanigan suivant afin d'obtenir une valeur dans un attribut de nœud:

MFnDoubleArrayData myArrayData;
MObject myArrayObj = myArrayData.create(myArray);   
MPlug myPlug = myNode.findPlug(attributeName);
myPlug.setValue(myArrayObj);

C'est fastidieux à écrire, j'ai donc écrit les fonctions d'assistance suivantes:

MPlug operator | (MFnDependencyNode& node, MObject& attribute){
    MStatus status;
    MPlug returnValue = node.findPlug(attribute, &status);
    return returnValue;
}

void operator << (MPlug& plug, MDoubleArray& doubleArray){
    MStatus status;
    MFnDoubleArrayData doubleArrayData;
    MObject doubleArrayObject = doubleArrayData.create(doubleArray, &status);
    status = plug.setValue(doubleArrayObject);
}

Et maintenant, je peux écrire le code depuis le début du message comme:

(myNode | attributeName) << myArray;

Le problème est qu'il ne compile pas en dehors de Visual C ++, car il essaie de lier la variable temporaire renvoyée par le | à la référence MPlug de l'opérateur <<. Je voudrais que ce soit une référence car ce code est appelé plusieurs fois et je préfère que MPlug ne soit pas copié autant. J'ai seulement besoin de l'objet temporaire pour vivre jusqu'à la fin de la deuxième fonction.

Eh bien, c'est mon scénario. Je pensais juste montrer un exemple où l'on aimerait faire ce qu'Alexey décrit. J'accueille toutes les critiques et suggestions!

Merci.

Robert Trussardi
la source