Un membre de la classe de référence const prolonge-t-il la vie d'un temporaire?

171

Pourquoi cela:

#include <string>
#include <iostream>
using namespace std;

class Sandbox
{
public:
    Sandbox(const string& n) : member(n) {}
    const string& member;
};

int main()
{
    Sandbox sandbox(string("four"));
    cout << "The answer is: " << sandbox.member << endl;
    return 0;
}

Donner la sortie de:

La réponse est:

Au lieu de:

La réponse est: quatre

Kyle
la source
39
Et juste pour plus de plaisir, si vous aviez écrit cout << "The answer is: " << Sandbox(string("four")).member << endl;, cela fonctionnera à coup sûr.
7
@RogerPate Pouvez-vous expliquer pourquoi?
Paolo M
16
Pour quelqu'un qui est curieux, par exemple Roger Pate a posté des œuvres parce que la chaîne ("quatre") est temporaire et que le temporaire est détruit à la fin de l'expression complète , donc dans son exemple lorsqu'il SandBox::memberest lu, la chaîne temporaire est toujours vivante .
PcAF
1
La question est la suivante: comme l'écriture de telles classes est dangereuse, existe-t-il un avertissement du compilateur contre le passage de temporaires à de telles classes , ou existe-t-il une directive de conception (dans Stroustroup?) Qui interdit l'écriture de classes qui stockent des références? Une directive de conception pour stocker des pointeurs au lieu de références serait préférable.
Grim Fandango
@PcAF: Pouvez-vous expliquer pourquoi le temporaire string("four")est détruit à la fin de l'expression complète, et non après la fermeture du Sandboxconstructeur? La réponse de Potatoswatter dit qu'une liaison temporaire à un membre de référence dans le ctor-initializer d'un constructeur (§12.6.2 [class.base.init]) persiste jusqu'à ce que le constructeur se termine.
Taylor Nichols

Réponses:

166

Seules les const références locales prolongent la durée de vie.

La norme spécifie un tel comportement dans le §8.5.3 / 5, [dcl.init.ref], la section sur les initialiseurs de déclarations de référence. La référence dans votre exemple est liée à l'argument du constructeur net devient invalide lorsque l'objet nest lié à est hors de portée.

L'extension de durée de vie n'est pas transitive via un argument de fonction. §12.2 / 5 [classe.temporaire]:

Le deuxième contexte est lorsqu'une référence est liée à un temporaire. Le temporaire auquel la référence est liée ou le temporaire qui est l'objet complet d'un sous-objet dont le temporaire est lié persiste pendant la durée de vie de la référence, sauf comme spécifié ci-dessous. Une liaison temporaire à un membre de référence dans le ctor-initializer d'un constructeur (§12.6.2 [class.base.init]) persiste jusqu'à ce que le constructeur se termine. Une liaison temporaire à un paramètre de référence dans un appel de fonction (§5.2.2 [expr.call]) persiste jusqu'à l'achèvement de l'expression complète contenant l'appel.

Potatoswatter
la source
49
Vous devriez également voir GotW # 88 pour une explication plus conviviale: herbsutter.com/2008/01/01
Nathan Ernst
1
Je pense que ce serait plus clair si la norme disait "Le deuxième contexte est lorsqu'une référence est liée à une valeur pr". Dans le code d'OP, vous pourriez dire que memberc'est lié à un temporaire, car l'initialisation memberavec des nmoyens de se lier memberau même objet nest liée à, et c'est en fait un objet temporaire dans ce cas.
MM
2
@MM Il y a des cas où les initialiseurs lvalue ou xvalue contenant une prvalue étendront la prvalue. Mon document de proposition P0066 passe en revue la situation.
Potatoswatter
1
Depuis C ++ 11, les références Rvalue prolongent également la durée de vie d'un temporaire sans nécessiter de constquaifier.
GetFree du
3
@KeNVinFavo oui, utiliser un objet mort est toujours UB
Potatoswatter
30

Voici le moyen le plus simple d'expliquer ce qui s'est passé:

Dans main (), vous avez créé une chaîne et l'avez passée au constructeur. Cette instance de chaîne n'existait que dans le constructeur. À l'intérieur du constructeur, vous avez affecté le membre pour qu'il pointe directement vers cette instance. Lorsque la portée a quitté le constructeur, l'instance de chaîne a été détruite et le membre a ensuite pointé vers un objet chaîne qui n'existait plus. Le fait que Sandbox.member pointe vers une référence en dehors de sa portée ne maintiendra pas ces instances externes dans la portée.

Si vous souhaitez corriger votre programme pour afficher le comportement souhaité, apportez les modifications suivantes:

int main()
{
    string temp = string("four");    
    Sandbox sandbox(temp);
    cout << sandbox.member << endl;
    return 0;
}

Désormais, temp passera hors de portée à la fin de main () plutôt qu'à la fin du constructeur. Cependant, c'est une mauvaise pratique. Votre variable membre ne doit jamais être une référence à une variable qui existe en dehors de l'instance. En pratique, vous ne savez jamais quand cette variable sortira du champ d'application.

Ce que je recommande est de définir Sandbox.member comme un const string member;Cela copiera les données du paramètre temporaire dans la variable membre au lieu d'assigner la variable membre comme paramètre temporaire lui-même.

Squirrelsama
la source
Si je fais ceci: cela fonctionnera- const string & temp = string("four"); Sandbox sandbox(temp); cout << sandbox.member << endl;t-il toujours?
Yves
@Thomas const string &temp = string("four");donne le même résultat que const string temp("four"); , sauf si vous utilisez decltype(temp)spécifiquement
MM
@MM Merci beaucoup maintenant, je comprends parfaitement cette question.
Yves
However, this is bad practice.- Pourquoi? Si la température et l'objet contenant utilisent le stockage automatique dans la même portée, n'est-il pas sûr à 100%? Et si vous ne le faites pas, que feriez-vous si la chaîne est trop grande et trop chère à copier?
max
2
@max, car la classe n'applique pas le passage temporaire pour avoir la bonne portée. Cela signifie qu'un jour vous pourriez oublier cette exigence, transmettre une valeur temporaire invalide et le compilateur ne vous avertira pas.
Alex Che
5

Techniquement parlant, ce programme n'est pas obligé de produire quoi que ce soit sur la sortie standard (qui est un flux tamponné pour commencer).

  • Le cout << "The answer is: "bit sera émis "The answer is: "dans le tampon de stdout.

  • Ensuite, le << sandbox.memberbit fournira la référence pendante dans operator << (ostream &, const std::string &), qui invoque un comportement indéfini .

Pour cette raison, rien n'est garanti. Le programme peut fonctionner en apparence correctement ou peut planter sans même vider stdout - ce qui signifie que le texte "La réponse est:" n'apparaîtra pas sur votre écran.

Tanz87
la source
2
Quand il y a UB, le comportement du programme entier n'est pas défini - il ne commence pas seulement à un moment particulier de l'exécution. Nous ne pouvons donc pas dire avec certitude que "The answer is: "cela sera écrit nulle part.
Toby Speight
0

Parce que votre chaîne temporaire est devenue hors de portée une fois le constructeur Sandbox retourné, et la pile occupée par elle a été récupérée à d'autres fins.

En règle générale, vous ne devez jamais conserver les références à long terme. Les références sont bonnes pour les arguments ou les variables locales, jamais pour les membres de la classe.

Fyodor Soikin
la source
7
«Jamais» est un mot terriblement fort.
Fred Larson
17
jamais de membres de classe sauf si vous devez conserver une référence à un objet. Il existe des cas où vous devez conserver des références à d'autres objets, et non des copies, car les références sont une solution plus claire que les pointeurs.
David Rodríguez - dribeas
0

vous parlez de quelque chose qui a disparu. Ce qui suit fonctionnera

#include <string>
#include <iostream>

class Sandbox
{

public:
    const string member = " "; //default to whatever is the requirement
    Sandbox(const string& n) : member(n) {}//a copy is made

};

int main()
{
    Sandbox sandbox(string("four"));
    std::cout << "The answer is: " << sandbox.member << std::endl;
    return 0;
}
pcodex
la source