O código a seguir parece bastante inofensivo à primeira vista. Um usuário usa a função bar()
para interagir com algumas funcionalidades da biblioteca. (Isso pode até ter funcionado por um longo tempo desde que bar()
retornou uma referência a um valor não temporário ou similar.) Agora, no entanto, está simplesmente retornando uma nova instância de B
. B
novamente tem uma função a()
que retorna uma referência a um objeto do tipo iterateable A
. O usuário deseja consultar este objeto, o que leva a um segfault, pois o B
objeto temporário retornado por bar()
é destruído antes do início da iteração.
Eu sou indeciso quem (biblioteca ou usuário) é o culpado por isso. Todas as classes fornecidas pela biblioteca parecem limpas para mim e certamente não estão fazendo nada diferente (retornando referências a membros, retornando instâncias de pilha, ...) do que muitos outros códigos existentes. O usuário parece não fazer nada de errado também, ele está apenas repetindo algum objeto sem fazer nada a respeito da vida útil do objeto.
(Uma pergunta relacionada pode ser: Se alguém estabelecer a regra geral de que o código não deve "basear-se na iteração" sobre algo recuperado por mais de uma chamada em cadeia no cabeçalho do loop, pois qualquer uma dessas chamadas pode retornar um rvalue?)
#include <algorithm>
#include <iostream>
// "Library code"
struct A
{
A():
v{0,1,2}
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
int * begin()
{
return &v[0];
}
int * end()
{
return &v[3];
}
int v[3];
};
struct B
{
A m_a;
A & a()
{
return m_a;
}
};
B bar()
{
return B();
}
// User code
int main()
{
for( auto i : bar().a() )
{
std::cout << i << std::endl;
}
}
Respostas:
Eu acho que o problema fundamental é uma combinação de recursos de linguagem (ou falta dela) do C ++. O código da biblioteca e o código do cliente são razoáveis (como evidenciado pelo fato de que o problema está longe de ser óbvio). Se a vida útil temporária
B
fosse adequada estendida (até o final do loop), não haveria problema.Tornar a vida temporária apenas o tempo suficiente, e não mais, é extremamente difícil. Nem mesmo um ad-hoc "todos os temporários envolvidos na criação do intervalo para um intervalo de transmissão ao vivo até o final do ciclo" ficaria sem efeitos colaterais. Considere o caso de
B::a()
retornar um intervalo independente doB
objeto por valor. Então o temporárioB
pode ser descartado imediatamente. Mesmo se alguém pudesse identificar com precisão os casos em que uma extensão vitalícia é necessária, como esses casos não são óbvios para os programadores, o efeito (destruidores chamado muito mais tarde) seria surpreendente e talvez uma fonte igualmente sutil de erros.Seria mais desejável detectar e proibir tais bobagens, forçando o programador a elevar explicitamente
bar()
a uma variável local. Isso não é possível no C ++ 11 e provavelmente nunca será possível porque requer anotações. Rust faz isso, onde a assinatura.a()
seria:Aqui
'x
está uma variável ou região vitalícia, que é um nome simbólico pelo período de tempo em que um recurso está disponível. Francamente, as vidas são difíceis de explicar - ou ainda não descobrimos a melhor explicação -, então vou me restringir ao mínimo necessário para este exemplo e encaminhar o leitor inclinado para a documentação oficial .O verificador de empréstimo notaria que o resultado da vida
bar().a()
útil precisa durar o ciclo. Formulada como uma restrição na vida'x
, podemos escrever:'loop <= 'x
. Ele também notaria que o receptor da chamada de método,,bar()
é temporário. Os dois ponteiros estão associados ao mesmo tempo de vida, portanto,'x <= 'temp
há outra restrição.Essas duas restrições são contraditórias! Precisamos ,
'loop <= 'x <= 'temp
mas'temp <= 'loop
que captura o problema com muita precisão. Por causa dos requisitos conflitantes, o código do buggy é rejeitado. Observe que essa é uma verificação em tempo de compilação e o código Rust geralmente resulta no mesmo código de máquina que o código C ++ equivalente; portanto, você não precisa pagar um custo em tempo de execução por isso.No entanto, esse é um grande recurso a ser adicionado a um idioma e só funciona se todo o código o usar. o design das APIs também é afetado (alguns designs que seriam muito perigosos em C ++ tornam-se práticos, outros não podem ser feitos para funcionar bem com a vida útil). Infelizmente, isso significa que não é prático adicionar retroativamente ao C ++ (ou a qualquer idioma). Em resumo, a falha está na inércia que as línguas bem-sucedidas têm e no fato de Bjarne em 1983 não ter a bola de cristal e a previsão de incorporar as lições dos últimos 30 anos de pesquisa e experiência em C ++ ;-)
Obviamente, isso não ajuda em nada a evitar o problema no futuro (a menos que você mude para Rust e nunca use C ++ novamente). Pode-se evitar expressões mais longas com várias chamadas de método encadeadas (o que é bastante limitante e nem mesmo corrige remotamente todos os problemas da vida). Ou pode-se tentar adotar uma política de propriedade mais disciplinada sem a assistência do compilador: documente claramente que
bar
retorna por valor e que o resultado deB::a()
não deve sobreviver mais doB
que oa()
invocado. Ao alterar uma função para retornar por valor, em vez de uma referência de vida mais longa, lembre-se de que essa é uma mudança de contrato . Ainda propenso a erros, mas pode acelerar o processo de identificação da causa quando isso acontece.fonte
Podemos resolver esse problema usando os recursos do C ++?
O C ++ 11 adicionou a função de membro ref-qualifiers, que permite restringir a categoria de valor da instância da classe (expressão) na qual a função de membro pode ser chamada. Por exemplo:
Ao chamar a
begin
função membro, sabemos que provavelmente também precisaremos chamar aend
função membro (ou algo parecidosize
, para obter o tamanho do intervalo). Isso requer que operemos com um valor l, pois precisamos corrigi-lo duas vezes. Portanto, você pode argumentar que essas funções de membro devem ser qualificadas para lvalue-ref.No entanto, isso pode não resolver o problema subjacente: alias. A função membro
begin
eend
alias do objeto ou os recursos gerenciados pelo objeto. Se substituirmosbegin
eend
por uma única funçãorange
, devemos fornecer uma que possa ser chamada em rvalues:Esse pode ser um caso de uso válido, mas a definição acima
range
não o permite. Como não podemos resolver o temporário após a chamada da função de membro, pode ser mais razoável retornar um contêiner, ou seja, um intervalo de propriedade:Aplicando isso ao caso do OP, e uma leve revisão do código
Essa função de membro altera a categoria de valor da expressão:
B()
é um prvalue, masB().a()
é um lvalue. Por outro lado,B().m_a
é um rvalue. Então, vamos começar tornando isso consistente. Existem duas maneiras de fazer isso:A segunda versão, como dito acima, corrigirá o problema no OP.
Além disso, podemos restringir
B
as funções de membro de:Isso não terá nenhum impacto no código do OP, pois o resultado da expressão após o
:
loop for baseado no intervalo está vinculado a uma variável de referência. E essa variável (como expressão usada para acessar suabegin
end
funções e membro) é um valor l.Obviamente, a questão é se a regra padrão deve ou não "funções de membro de alias em rvalues devem retornar um objeto que possui todos os seus recursos, a menos que haja uma boa razão para não" . O alias que ele retorna pode ser legalmente usado, mas é perigoso da maneira que você o experimenta: não pode ser usado para prolongar a vida útil do temporário "pai":
No C ++ 2a, acho que você deve solucionar esse problema (ou semelhante) da seguinte maneira:
em vez dos OP
A solução alternativa especifica manualmente que o tempo de vida de
b
é o bloco inteiro do loop for.Proposta que introduziu esta declaração de inicialização
Demonstração ao vivo
fonte