A boot implica conversão lvalue para rvalue? É int x = x; UB?

O padrão C ++ contém um exemplo semi-famoso de pesquisa de nome “surpreendente” em 3.3.2, “Ponto de declaração”:

int x = x; 

Isso inicializa x com si mesmo, que (sendo um tipo primitivo) é não inicializado e, portanto, tem um valor indeterminado (supondo que seja uma variável automática).

Isso é realmente um comportamento indefinido?

De acordo com 4.1 “Lvalue para rvalue conversão”, é comportamento indefinido para executar lvalue para rvalue conversão em um valor não inicializado. A mão direita x sofre essa conversão? Em caso afirmativo, o exemplo teria um comportamento indefinido?

ATUALIZAÇÃO: Após a discussão nos comentários, adicionei mais algumas evidências no final desta resposta.


Disclaimer : Eu admito que esta resposta é bastante especulativa. A formulação atual do C ++ 11 Standard, por outro lado, não parece permitir uma resposta mais formal.


No contexto desta session de perguntas e respostas , surgiu que o C ++ 11 Standard não especifica formalmente quais categorias de valor são esperadas para cada construção de linguagem. A seguir, vou focar principalmente em operadores internos , embora a questão seja sobre inicializadores . Eventualmente, vou acabar estendendo as conclusões que tirei para o caso dos operadores para o caso dos inicializadores.

No caso de operadores embutidos, apesar da falta de uma especificação formal, evidências (não-normativas) são encontradas no Padrão de que a especificação pretendida é permitir que os valores sejam esperados sempre que um valor for necessário, e quando não especificado caso contrário .

Por exemplo, uma nota no Parágrafo 3.10 / 1 diz:

A discussão de cada operador integrado na Cláusula 5 indica a categoria do valor que ele produz e as categorias de valor dos operandos esperados. Por exemplo, os operadores de atribuição internos esperam que o operando à esquerda seja um lvalue e que o operando da direita seja um prvalue e produza um lvalue como resultado. Os operadores definidos pelo usuário são funções e as categorias de valores esperadas e produzidas são determinadas por seus tipos de parâmetro e retorno

A seção 5.17 dos operadores de atribuição, por outro lado, não menciona isso. No entanto, a possibilidade de realizar uma conversão de lvalor para rvalue é mencionada, novamente em uma nota (Parágrafo 5.17 / 1):

Portanto, uma chamada de function não deve intervir entre a conversão de lvalor para rvalue e o efeito colateral associado a um único operador de atribuição de compostos

Claro que, se nenhum valor fosse esperado, esta nota não teria sentido.

Outra evidência é encontrada em 4/8, como apontado por Johannes Schaub nos comentários para o link Q & A:

Existem alguns contextos em que determinadas conversões são suprimidas. Por exemplo, a conversão de lvalue para rvalue não é feita no operando do operador unário. Exceções específicas são dadas nas descrições desses operadores e contextos.

Isso parece implicar que a conversão de lvalue para rvalue é executada em todos os operandos de operadores internos, exceto quando especificado de outra forma. Isso significaria, por sua vez, que rvalues ​​são esperados como operandos de operadores internos, a menos que especificado de outra forma.


CONJETURA:

Mesmo que a boot não seja uma atribuição e, portanto, os operadores não entrem na discussão, suspeito que essa área da especificação seja afetada pelo mesmo problema descrito acima.

Rastreios que suportam essa crença podem ser encontrados mesmo no Parágrafo 8.5.2 / 5, sobre a boot de referências (para as quais o valor da expressão inicializadora lvalue não é necessário):

As conversões padrão lvalue-to-rvalue (4.1), array-para-ponteiro (4.2) e function-para-ponteiro (4.3) não são necessárias e, portanto, são suprimidas, quando tais vinculações diretas a lvalues ​​são feitas.

A palavra “usual” parece implicar que ao inicializar objects que não são de tipo de referência, a conversão de lvalue para rvalue deve ser aplicada.

Portanto, acredito que, embora os requisitos sobre a categoria de valor esperado dos inicializadores sejam mal especificados (se não estiverem completamente ausentes), com base nas evidências fornecidas, faz sentido supor que a especificação pretendida é a seguinte:

Sempre que um valor for exigido por uma construção de linguagem, um valor é esperado, a menos que seja especificado de outra forma .

Sob essa suposição, uma conversão de lvalor para rvalue seria necessária em seu exemplo, e isso levaria ao comportamento indefinido.


EVIDÊNCIA ADICIONAL:

Apenas para fornecer mais evidências para apoiar esta conjectura, vamos supor que está errado , de modo que nenhuma conversão lvalue-para-rvalue seja realmente necessária para a boot da cópia, e considere o seguinte código (graças ao jogojapan por contribuir):

 int y; int x = y; // No UB short t; int u = t; // UB! (Do not like this non-uniformity, but could accept it) int z; z = x; // No UB (x is not uninitialized) z = y; // UB! (Assuming assignment operators expect a prvalue, see above) // This would be very counterintuitive, since x == y 

Esse comportamento não uniforme não faz muito sentido para mim. O que faz mais sentido no IMO é que, sempre que um valor é necessário, um valor é esperado.

Além disso, como Jesse Good aponta corretamente em sua resposta, o Parágrafo chave do C ++ Standard é 8.5 / 16:

– Caso contrário, o valor inicial do object que está sendo inicializado é o valor (possivelmente convertido) da expressão inicializadora . Serão usadas conversões padrão (cláusula 4), se necessário , para converter a expressão inicializadora na versão não qualificada cv do tipo de destino; não são consideradas conversões definidas pelo usuário. Se a conversão não pode ser feita, a boot é mal formada. [Nota: Uma expressão do tipo “cv1 T” pode inicializar um object do tipo “cv2 T” independentemente dos cv-qualificadores cv1 e cv2.

No entanto, enquanto Jesse foca principalmente no bit ” se necessário “, eu também gostaria de enfatizar a palavra ” type “. O parágrafo acima menciona que conversões padrão serão usadas ” se necessário ” para converter para o tipo de destino, mas não diz nada sobre conversões de categoria :

  1. As conversões de categoria serão realizadas, se necessário?
  2. Eles são necessários?

Para o que diz respeito à segunda pergunta, como discutido na parte original da resposta, o C ++ 11 Standard atualmente não especifica se as conversões de categoria são necessárias ou não, porque em nenhum lugar é mencionado se a boot de cópia espera um valor como inicializador . Assim, uma resposta clara é impossível de dar. No entanto, acredito ter fornecido evidência suficiente para supor que esta seja a especificação pretendida , de modo que a resposta seja “sim”.

Quanto à primeira pergunta, parece-me razoável que a resposta seja “sim” também. Se fosse “Não”, obviamente os programas corretos seriam mal formados:

 int y = 0; int x = y; // y is lvalue, prvalue expected (assuming the conjecture is correct) 

Para resumir (A1 = ” Resposta à questão 1 “, A2 = ” Resposta à questão 2 “):

  | A2 = Yes | A2 = No | ---------|------------|---------| A1 = Yes | UB | No UB | A1 = No | ill-formed | No UB | --------------------------------- 

Se A2 for “Não”, A1 não importa: não há UB, mas as situações bizarras do primeiro exemplo (por exemplo, z = y dando UB, mas não z = x , embora x == y ) apareçam. Se A2 for “Sim”, por outro lado, A1 se torna crucial; No entanto, provas suficientes foram dadas para provar que seria “sim”.

Portanto, minha tese é que A1 = “Sim” e A2 = “Sim”, e devemos ter um comportamento indefinido .


EVIDÊNCIA FUTURA:

Este relatório de defeito (cortesia de Jesse Good ) propõe uma alteração que visa dar Comportamento Indefinido neste caso:

[…] Além disso, o parágrafo 1 do [conv.lval] 4.1 diz que a aplicação da conversão de lvalue para rvalue a um “object [que] não é inicializado” resulta em um comportamento indefinido; isso deve ser reformulado em termos de um object com um valor indeterminado .

Em particular, a redação proposta para o parágrafo 4.1 diz:

Quando uma conversão lvalue-para-rvalue ocorre em um operando não avaliado ou em uma subexpressão dele (cláusula 5 [expr]), o valor contido no object referenciado não é acessado. Em todos os outros casos, o resultado da conversão é determinado de acordo com as seguintes regras:

– Se T for (possivelmente cv-qualificado) std :: nullptr_t, o resultado é uma constante de ponteiro nulo (4.10 [conv.ptr]).

– Caso contrário, se o glvalue T tiver um tipo de class, a conversão copia-inicializa um temporário do tipo T do glvalue e o resultado da conversão é um prvalue para o temporário.

– Caso contrário, se o object ao qual o glvalue se refere contiver um valor de ponteiro inválido (3.7.4.2 [basic.stc.dynamic.deallocation], 3.7.4.3 [basic.stc.dynamic.safety]), o comportamento será definido pela implementação .

– Caso contrário, se T for um tipo de caractere não assinado (possivelmente cv-qualificado) (3.9.1 [basic.fundamental]), e o object ao qual o glvalue se refere contém um valor indeterminado (5.3.4 [expr.new], 8.5 [dcl.init], 12.6.2 [class.base.init]), e esse object não tem duração de armazenamento automático ou o glvalue era o operando de um operador unário & ou estava ligado a uma referência, o resultado é um valor não especificado. [Nota de rodapé: O valor pode ser diferente toda vez que a conversão de lvalue para rvalue é aplicada ao object. Um object char não assinado com valor indeterminado alocado para um registrador pode interceptar. – terminar nota de rodapé]

Caso contrário, se o object ao qual o glvalue se refere contém um valor indeterminado, o comportamento é indefinido.

– Caso contrário, se o glvalue tiver (possivelmente cv-qualificado) o tipo std :: nullptr_t, o resultado do prvalue é uma constante de ponteiro nulo (4.10 [conv.ptr]). Caso contrário, o valor contido no object indicado pelo glvalue é o resultado do prvalue.

Uma sequência de conversão implícita de uma expressão e para o tipo T é definida como sendo equivalente à seguinte declaração, usando t como o resultado da conversão (categoria de valor de módulo, que será definida dependendo de T ), 4p3 e 4p6

 T t = e; 

O efeito de qualquer conversão implícita é o mesmo que executar a declaração e boot correspondentes e depois usar a variável temporária como resultado da conversão.

Na cláusula 4, a conversão de uma expressão em um tipo sempre produz expressões com uma propriedade específica. Por exemplo, a conversão de 0 para int* produz um valor de ponteiro nulo e não apenas um valor de ponteiro arbitrário. A categoria de valor também é uma propriedade específica de uma expressão e seu resultado é definido da seguinte forma

O resultado é um lvalue se T for um tipo de referência lvalue ou uma referência rvalue para um tipo de function (8.3.2), um valor x se T for uma referência rvalue para um tipo de object e um prvalue caso contrário.

Por isso sabemos que em int t = e; , o resultado da sequência de conversão é um prvalue, porque int é um tipo não referência. Portanto, se fornecermos um valor ótimo, precisamos obviamente de uma conversão. 3.10p2 esclarece ainda que para não deixar dúvidas

Sempre que um glvalue aparecer em um contexto em que um prvalue é esperado, o glvalue é convertido em um prvalue; ver 4.1, 4.2 e 4.3.

isso não é um comportamento indefinido. Você simplesmente não conhece seus valores específicos, porque não há boot. Se a variável for global e integrada, o compilador a colocará no valor correto. Se a variável é local, então o compilador não inicializa, então todas as variables ​​são inicializadas para você, não confie no compilador.

O comportamento não é indefinido. A variável é não inicializada e permanece com qualquer valor random inicializado de valores não inicializados. Um exemplo do naipe de teste clan’g:

 int test7b(int y) { int x = x; // expected-note{{variable 'x' is declared here}} if (y) x = 1; // Warn with "may be uninitialized" here (not "is sometimes uninitialized"), // since the self-initialization is intended to suppress a -Wuninitialized // warning. return x; // expected-warning{{variable 'x' may be uninitialized when used here}} } 

Que você pode encontrar nos testes clang / test / Sema / uninit-variables.c para este caso explicitamente.