Um membro da classe de referência constante prolonga a vida de um temporário?

171

Por que isso:

#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;
}

Dê saída de:

A resposta é:

Ao invés de:

A resposta é: quatro

Kyle
fonte
39
E apenas para mais diversão, se você tivesse escrito cout << "The answer is: " << Sandbox(string("four")).member << endl;, seria garantido que funcionaria.
7
@RogerPate Você poderia explicar o porquê?
Paolo M
16
Para alguém curioso, o exemplo que Roger Pate postou funciona porque a string ("quatro") é temporária e essa temporária é destruída no final da expressão total ; portanto, no exemplo em que SandBox::memberé lida, a string temporária ainda está ativa .
PcAF 23/05
1
A pergunta é: como escrever essas classes é perigoso, existe um aviso do compilador contra a passagem de temporários para essas classes ou há uma diretriz de design (no Stroustroup?) Que proíbe a escrita de classes que armazenam referências? Uma diretriz de design para armazenar ponteiros em vez de referências seria melhor.
Grim Fandango
@PcAF: Você poderia explicar por que o temporário string("four")é destruído no final da expressão completa e não depois que o Sandboxconstrutor sai? A resposta de Potatoswatter diz que Um
Taylor Nichols

Respostas:

166

Somente referências locais const prolongam a vida útil.

O padrão especifica esse comportamento em §8.5.3 / 5, [dcl.init.ref], a seção sobre inicializadores de declarações de referência. A referência no seu exemplo está vinculada ao argumento do construtor ne se torna inválida quando o objeto nestá fora do escopo.

A extensão da vida útil não é transitiva por meio de um argumento de função. §12.2 / 5 [class.temporary]:

O segundo contexto é quando uma referência é vinculada a um temporário. O temporário ao qual a referência está vinculada ou o temporário que é o objeto completo de um subobjeto do qual o temporário está vinculado persistem durante toda a vida útil da referência, exceto conforme especificado abaixo. Um vínculo temporário a um membro de referência no ctor-inicializador de um construtor (§12.6.2 [class.base.init]) persiste até que o construtor saia. Um limite temporário para um parâmetro de referência em uma chamada de função (§5.2.2 [expr.call]) persiste até a conclusão da expressão completa que contém a chamada.

Potatoswatter
fonte
49
Você também deve ver GotW # 88 para uma explicação mais amigável-humana: herbsutter.com/2008/01/01/...
Nathan Ernst
1
Eu acho que seria mais claro se o padrão dissesse "O segundo contexto é quando uma referência está vinculada a um valor". No código do OP, você poderia dizer que memberestá vinculado a um temporário, porque a inicialização membercom nmeios para vincular memberao mesmo objeto nestá vinculada e, de fato, é um objeto temporário nesse caso.
MM
2
@MM Há casos em que os inicializadores lvalue ou xvalue que contêm um prvalue estendem o prvalue. O documento da minha proposta P0066 analisa a situação.
Potatoswatter
1
A partir do C ++ 11, as referências Rvalue também prolongam a vida útil de um temporário sem a necessidade de um constquaificador.
GetFree
3
@KeNVinFavo sim, usando um objeto morto é sempre UB
Potatoswatter
30

Aqui está a maneira mais simples de explicar o que aconteceu:

Em main (), você criou uma string e a passou para o construtor. Essa instância de cadeia existia apenas dentro do construtor. Dentro do construtor, você designou um membro para apontar diretamente para esta instância. Quando o escopo deixou o construtor, a instância da string foi destruída e o membro apontou para um objeto de string que não existia mais. Fazer com que Sandbox.member aponte para uma referência fora de seu escopo não manterá essas instâncias externas no escopo.

Se você deseja corrigir o seu programa para exibir o comportamento desejado, faça as seguintes alterações:

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

Agora o temp ficará fora do escopo no final de main () em vez de no final do construtor. No entanto, isso é uma prática ruim. Sua variável de membro nunca deve ser uma referência a uma variável que existe fora da instância. Na prática, você nunca sabe quando essa variável ficará fora do escopo.

O que eu recomendo é definir Sandbox.member como um const string member;Isso copiará os dados do parâmetro temporário na variável membro em vez de atribuir a variável membro como o próprio parâmetro temporário.

Squirrelsama
fonte
Se eu fizer isso: const string & temp = string("four"); Sandbox sandbox(temp); cout << sandbox.member << endl;ainda funcionará?
Yves
@Thomas const string &temp = string("four");dá o mesmo resultado que const string temp("four"); , a menos que você usa decltype(temp)especificamente
MM
@ MM Muito obrigado agora eu entendo totalmente esta questão.
Yves
However, this is bad practice.- porque? Se o temp e o objeto contido usam armazenamento automático no mesmo escopo, não é 100% seguro? E se você não fizer isso, o que faria se a string fosse muito grande e muito cara para copiar?
máximo
2
@max, porque a classe não impõe o passado temporariamente para ter o escopo correto. Isso significa que um dia você pode esquecer esse requisito, passar um valor temporário inválido e o compilador não avisará.
Alex Che #
5

Tecnicamente falando, não é necessário que esse programa produza algo na saída padrão (que é um fluxo em buffer para começar).

  • O cout << "The answer is: "bit será emitido "The answer is: "no buffer do stdout.

  • Em seguida, o << sandbox.memberbit fornecerá a referência pendente operator << (ostream &, const std::string &), que invoca um comportamento indefinido .

Por isso, nada garante que isso aconteça. O programa pode funcionar aparentemente bem ou pode falhar sem liberar o stdout - o que significa que o texto "A resposta é:" não aparece na tela.

Tanz87
fonte
2
Quando há UB, o comportamento do programa inteiro é indefinido - ele não é iniciado apenas em um ponto específico da execução. Portanto, não podemos dizer com certeza que "The answer is: "será escrito em qualquer lugar.
Toby Speight
0

Como sua sequência temporária ficou fora do escopo quando o construtor Sandbox retornou e a pilha ocupada por ela foi recuperada para outros fins.

Geralmente, você nunca deve reter referências a longo prazo. As referências são boas para argumentos ou variáveis ​​locais, nunca para membros da classe.

Fyodor Soikin
fonte
7
"Nunca" é uma palavra muito forte.
Fred Larson
17
nunca os alunos, a menos que você precise manter uma referência a um objeto. Há casos em que você precisa manter referências a outros objetos, e não cópias, pois essas referências são uma solução mais clara que os ponteiros.
David Rodríguez - dribeas
0

você está se referindo a algo que desapareceu. O seguinte funcionará

#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
fonte