Tudo bem retornar o valor do argumento padrão por referência const?

26

Tudo bem retornar o valor do argumento padrão por referência const, como nos exemplos abaixo:

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");
}
Coração congelado
fonte
3
Teste simples: substitua std::stringpor uma classe própria para poder acompanhar a construção e a destruição.
precisa saber é o seguinte
11
@ user4581301 Se a sequência estiver correta, não provaria que a construção está correta.
Peter - Restabelece Monica
6
@ user4581301 "Pareceu funcionar quando tentei" é a pior coisa absoluta sobre comportamento indefinido
HerrJoebob 01/11/19
Deve-se notar que a pergunta é um petisco enganoso em sua redação. Você não está retornando o valor de um argumento padrão por referência const, mas está retornando uma referência const a uma referência const (... a um argumento padrão).
Damon
2
@HerrJoebob Concordo com a afirmação 100%, mas não com o contexto em que você a está usando. Da maneira como eu a li, esta pergunta resolve como "Quando o tempo de vida do objeto acabou?" descobrir quando o destruidor é chamado é uma boa maneira de fazer isso. Para uma variável automática, o destruidor deve ser chamado na hora certa ou você tem grandes problemas.
user4581301

Respostas:

18

No seu código, ambos s1e s3são referências pendentes. s2e s4estão bem.

Na primeira chamada, o std::stringobjeto vazio temporário criado a partir do argumento padrão será criado no contexto da expressão que contém a chamada. Portanto, ele morrerá no final da definição de s1, o que deixa s1pendente.

Na segunda chamada, o std::stringobjeto temporário é usado para inicializar s2e depois morre.

Na terceira chamada, a string literal "s"é usada para criar um std::stringobjeto temporário e que também morre no final da definição de s3, deixando s3dangling.

Na quarta chamada, o std::stringobjeto temporário com valor "s"é usado para inicializar s4e depois morre.

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

Um objeto temporário vinculado a um parâmetro de referência em uma chamada de função (8.2.2) persiste até a conclusão da expressão completa que contém a chamada.

Brian
fonte
11
A parte interessante da resposta é a afirmação de que o argumento padrão será criado no contexto do chamador. Isso parece ser apoiado pela citação padrão de Guillaume.
Peter - Restabelece Monica
2
@ Peter-ReinstateMonica Veja [expr.call] / 4, "... A inicialização e a destruição de cada parâmetro ocorrem dentro do contexto da função de chamada. ..."
Brian
8

Não é seguro :

Em geral, o tempo de vida de um temporário não pode ser estendido ainda mais "transmitindo-o": uma segunda referência, inicializada a partir da referência à qual o temporário estava vinculado, não afeta seu tempo de vida.

Esquecimento
fonte
Então você acha que std::string s2 = foo();é válido (afinal, nenhuma referência é passada explicitamente)?
Peter - Restabelece Monica
11
@ Peter-ReinstateMonica que um está seguro como novo objeto será construído. Minha resposta é apenas sobre a expansão da vida. As outras duas respostas já cobriam tudo. Eu não repetiria no meu.
esquecimento
5

Depende do que você faz com a sequência posteriormente.

Se sua pergunta for, meu código está correto? então sim é isso.

De [dcl.fct.default] / 2

[ Exemplo : a declaração

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

declara uma função que pode ser chamada com zero, um ou dois argumentos do tipo int. Pode ser chamado de qualquer uma das seguintes maneiras:

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

As duas últimas chamadas são equivalentes a point(1,4)e point(3,4), respectivamente. - exemplo final ]

Portanto, seu código é efetivamente equivalente a:

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

Todo o seu código está correto, mas não há extensão de vida útil de referência em nenhum desses casos, pois o tipo de retorno é uma referência.

Como você chama uma função com um temporário, a vida útil da string retornada não prolongará a instrução.

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

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

Seu exemplo com s2é bom, já que você copia (ou move) do temporário antes do final do processo. s3tem o mesmo problema que s1.

Guillaume Racicot
fonte