Est-il correct de renvoyer la valeur de l'argument par défaut par référence const?

26

Est-il correct de renvoyer la valeur de l'argument par défaut par référence const comme dans les exemples ci-dessous:

https://coliru.stacked-crooked.com/a/ff76e060a007723b

#include <string>

const std::string& foo(const std::string& s = std::string(""))
{
    return s;
}

int main()
{
    const std::string& s1 = foo();
    std::string s2 = foo();

    const std::string& s3 = foo("s");
    std::string s4 = foo("s");
}
Cœur gelé
la source
3
Test simple: remplacez-le std::stringpar une classe de votre choix pour suivre la construction et la destruction.
user4581301
1
@ user4581301 Si la séquence est correcte, cela ne prouverait cependant pas que la construction est correcte.
Peter - Rétablir Monica
6
@ user4581301 "Cela semblait fonctionner quand je l'ai essayé" est la pire chose à propos d'un comportement indéfini
HerrJoebob
Il convient de noter que la question est une friandise trompeuse dans sa formulation. Vous ne renvoyez pas la valeur d'un argument par défaut par référence const, mais vous renvoyez une référence const à une référence const (... à un argument par défaut).
Damon
2
@HerrJoebob D'accord avec l'énoncé à 100%, mais pas le contexte dans lequel vous l'utilisez. La façon dont je le lis, cette question se résout à "Quand la vie de l'objet est-elle terminée?" déterminer quand le destructeur est appelé est un bon moyen de le faire. Pour une variable automatique, le destructeur doit être appelé à temps ou vous avez de gros problèmes.
user4581301

Réponses:

18

Dans votre code, les deux s1et s3sont des références pendantes. s2et s4sont ok.

Dans le premier appel, l' std::stringobjet vide temporaire créé à partir de l'argument par défaut sera créé dans le contexte de l'expression contenant l'appel. Par conséquent, il mourra à la fin de la définition de s1, ce qui laisse s1pendant.

Dans le deuxième appel, l' std::stringobjet temporaire est utilisé pour initialisers2 , puis il meurt.

Dans le troisième appel, le littéral de chaîne "s"est utilisé pour créer un std::stringobjet temporaire et qui meurt également à la fin de la définition de s3, laissant s3pendre.

Dans le quatrième appel, l' std::stringobjet temporaire avec valeur "s"est utilisé pour initialiser s4puis il meurt.

Voir C ++ 17 [class.temporary] /6.1

Un objet temporaire lié à un paramètre de référence dans un appel de fonction (8.2.2) persiste jusqu'à la fin de l'expression complète contenant l'appel.

Brian
la source
1
La partie intéressante de la réponse est l'affirmation selon laquelle l'argument par défaut sera créé dans le contexte de l'appelant. Cela semble être soutenu par la citation standard de Guillaume.
Peter - Rétablir Monica
2
@ Peter-ReinstateMonica Voir [expr.call] / 4, "... L'initialisation et la destruction de chaque paramètre se produit dans le contexte de la fonction appelante. ..."
Brian
8

Ce n'est pas sûr :

En général, la durée de vie d'un temporaire ne peut pas être prolongée davantage en la "transmettant": une deuxième référence, initialisée à partir de la référence à laquelle le temporaire était lié, n'affecte pas sa durée de vie.

Oubli
la source
Alors, pensez-vous que std::string s2 = foo();c'est valide (après tout, aucune référence n'est transmise explicitement)?
Peter - Rétablir Monica
1
@ Peter-ReinstateMonica que l'on est en sécurité car un nouvel objet sera construit. Ma réponse concerne simplement l'extension de la durée de vie. Les deux autres réponses couvraient déjà tout. Je ne répéterais pas dans le mien.
Oblivion
5

Cela dépend ensuite de ce que vous faites avec la chaîne.

Si votre question est, mon code est-il correct? alors oui c'est ça.

Depuis [dcl.fct.default] / 2

[ Exemple : la déclaration

void point(int = 3, int = 4);

déclare une fonction qui peut être appelée avec zéro, un ou deux arguments de type int. Il peut être appelé de l'une des manières suivantes:

point(1,2);  point(1);  point();

Les deux derniers appels sont équivalents à point(1,4)et point(3,4), respectivement. - fin exemple ]

Votre code équivaut donc à:

const std::string& s1 = foo(std::string(""));
std::string s2 = foo(std::string(""));

Tout votre code est correct, mais il n'y a aucune extension de durée de vie de référence dans aucun de ces cas, car le type de retour est une référence.

Puisque vous appelez une fonction avec une valeur temporaire, la durée de vie de la chaîne renvoyée ne prolongera pas l'instruction.

const std::string& s1 = foo(std::string("")); // okay

s1; // not okay, s1 is dead. s1 is the temporary.

Votre exemple avec s2est correct puisque vous copiez (ou déplacez) du temporaire avant la fin du satement. s3a le même problème que s1.

Guillaume Racicot
la source