¿Está bien devolver el valor del argumento predeterminado por referencia constante?

26

¿Está bien devolver el valor del argumento predeterminado por referencia constante como en los ejemplos a continuación:

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");
}
Corazón congelado
fuente
3
Prueba simple: reemplácela std::stringcon una clase propia para que pueda rastrear la construcción y la destrucción.
user4581301
1
@ user4581301 Si la secuencia es correcta, no probaría que la construcción está bien.
Peter - Restablece a Mónica el
66
@ user4581301 "Parecía funcionar cuando lo intenté" es lo peor del comportamiento indefinido
HerrJoebob
Cabe señalar que la pregunta es un tidbit engañoso en su redacción. No está devolviendo el valor de un argumento predeterminado por referencia constante, pero está devolviendo una referencia constante a una referencia constante (... a un argumento predeterminado).
Damon el
2
@HerrJoebob De acuerdo con el enunciado 100%, pero no con el contexto en el que lo está utilizando. La forma en que lo leí esta pregunta se resuelve en "¿Cuándo termina la vida útil del objeto?" averiguar cuándo se llama al destructor es una buena manera de hacerlo. Para una variable automática, el destructor debe llamarse justo a tiempo o tiene grandes problemas.
user4581301

Respuestas:

18

En su código, ambos s1y s3son referencias colgantes. s2y s4están bien

En la primera llamada, el std::stringobjeto vacío temporal creado a partir del argumento predeterminado se creará en el contexto de la expresión que contiene la llamada. Por lo tanto, morirá al final de la definición de s1, lo que deja s1colgando.

En la segunda llamada, el std::stringobjeto temporal se usa para inicializar s2, luego muere.

En la tercera llamada, el literal de cadena "s"se usa para crear un std::stringobjeto temporal y que también muere al final de la definición de s3, dejando s3colgando.

En la cuarta llamada, el std::stringobjeto temporal con valor "s"se usa para inicializar s4y luego muere.

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

Un objeto temporal vinculado a un parámetro de referencia en una llamada de función (8.2.2) persiste hasta la finalización de la expresión completa que contiene la llamada.

Brian
fuente
1
La parte interesante de la respuesta es la afirmación de que el argumento predeterminado se creará en el contexto de la persona que llama. Eso parece estar respaldado por la cita estándar de Guillaume.
Peter - Restablece a Mónica el
2
@ Peter-ReinstateMonica Vea [expr.call] / 4, "... La inicialización y destrucción de cada parámetro ocurre dentro del contexto de la función de llamada ..."
Brian
8

No es seguro :

En general, la vida útil de un temporal no puede extenderse aún más "pasándola": una segunda referencia, inicializada a partir de la referencia a la que estaba vinculado el temporal, no afecta su vida útil.

Olvido
fuente
Entonces, ¿crees que std::string s2 = foo();es válido (después de todo, no se pasa ninguna referencia explícitamente)?
Peter - Restablece a Monica
1
@ Peter-ReinstateMonica que uno está a salvo ya que se construirá un nuevo objeto. Mi respuesta es simplemente sobre la expansión de por vida. Las otras dos respuestas ya cubrieron todo. No repetiría en el mío.
Olvido el
5

Depende de lo que hagas con la cuerda después.

Si su pregunta es ¿ es correcto mi código? entonces sí lo es.

De [dcl.fct.default] / 2

[ Ejemplo : la declaración

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

declara una función que se puede llamar con cero, uno o dos argumentos de tipo int. Se puede llamar de cualquiera de estas formas:

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

Las dos últimas llamadas son equivalentes a point(1,4)y point(3,4), respectivamente. - ejemplo final ]

Entonces su código es efectivamente equivalente a:

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

Todo su código es correcto, pero no hay extensión de duración de referencia en ninguno de estos casos, ya que el tipo de retorno es una referencia.

Como llama a una función con un carácter temporal, la vida útil de la cadena devuelta no extenderá la instrucción.

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

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

Su ejemplo con s2está bien ya que copia (o mueve) desde el temporal antes del final del satement. s3tiene el mismo problema que s1.

Racicot Guillaume
fuente