Em C, usar uma variável não inicializada é um comportamento indefinido. Incluindo:
int x;
int y = x;
No entanto, suponha que eu tenha o seguinte:
struct Struct {
int a;
int b;
};
struct Struct s;
s.a = 1;
struct Struct s0 = s;
return s; // Assuming an enclosing function with the correct return type
O código acima invoca um comportamento indefinido, no caso de alguns elementos, mas não todos, terem sido atribuídos?
Isto não é abordado adequadamente pelo padrão C, mas uma implementação C razoável suporta o uso de uma estrutura inteira quando alguns membros não são inicializados. E há uma solução simples para qualquer implementação C.
Observe que “Em C, usar uma variável não inicializada é um comportamento indefinido” não é totalmente verdade. O valor de um objeto não inicializado com duração de armazenamento automático é indeterminado de acordo com C 2018 6.7.9 10. Usar o valor de tal objeto tem comportamento indefinido se seu endereço não for usado, de acordo com C 2018 6.3.2.1 2:
Assim, para garantir que a utilização de uma estrutura não tenha comportamento indefinido, basta obter o seu endereço. Digamos que você tenha algum objeto de estrutura
s
. Então podemos pegar seu endereço (e descartá-lo) simplesmente inserindo(void) &s;
o código. Ao obter seu endereço, evitamos que o objeto seja possivelmente declaradoregister
, portanto 6.3.2.1 2 não se aplicará.Ainda pode haver alguma preocupação de que o uso de um objeto com valor indeterminado possa encontrar uma representação trap. No entanto, isso não ocorre para estruturas, conforme 6.2.6.1 2:
Esse parágrafo nos diz que o comitê C pretendia que os programas C fossem capazes de usar estruturas inteiras, mesmo que seus membros não fossem todos inicializados. 6.3.2.1 2, sobre comportamento indefinido para alguns objetos não inicializados, foi adicionado posteriormente para fornecer um recurso da Hewlett-Packard que permite ao processador rastrear registros não inicializados. Quando essa adição foi feita, o comitê não adicionou uma declaração semelhante a 6.2.6.1 2, informando-nos que ela não se aplicaria a uma estrutura.
Minha opinião seria que 6.2.6.1 2 indica que a intenção é que seja seguro copiar estruturas inteiras e 6.3.2.1 2 não pretende mudar isso. Porém, para ser totalmente seguro, tomar o endereço da estrutura conforme explicado acima garante que o acesso à estrutura não terá um comportamento indefinido, independentemente da interpretação do padrão a esse respeito.
A questão real e a questão pretendida parecem ser diferentes. Respondendo à pergunta real, a resposta curta é não, ele nem compila - portanto, não pode criar nenhum comportamento indefinido.
Parece que a pergunta pretendida é: "o retorno de uma estrutura parcialmente atribuída causará um comportamento indefinido?" O retorno implica que isso está dentro de uma função, então talvez seja uma complicação indesejada, mas vamos pensar nisso por um momento e agrupar o código original da operação em uma função e alguns elementos extras (como um main) para pontos de discussão :
Para começar, a primeira cópia de
s
intos0
causará uma cópia, o que significa que cada campo será acessado, o que significa que você tem um comportamento indefinido aqui. Supondo que isso não cause uma falha no seu compilador específico, o retorno também será um comportamento indefinido.Abaixo de tudo isso, presumindo que não haja travamentos porque o compilador o tratou de qualquer maneira, a
bob.b = 2
atribuição torna tudo OK para a instrução print. Assim, você pode se safar de um comportamento indefinido se o seu compilador for cúmplice do código incorreto. Mas sem essa atribuição extra, você também teria um comportamento indefinido no acesso naprintf()
chamada, e com o compilador cúmplice ou não, o problema finalmente ficaria evidente.É claro que ponteiros para estruturas também são frequentemente usados, e isso fica mais complicado... com os ponteiros, você está apenas copiando um endereço, portanto não terá um comportamento indefinido até acessar os campos. Claro, você também deve considerar cópia profunda versus cópia superficial, e se você mantiver
s
como uma variável local para essa função, você terá OUTRO comportamento indefinido porque agora você terá uma desreferência de ponteiro ilegal, pois estava apontando para empilhar isso estará fora do escopo quando você voltar aomain
. Mas, se você colocar essa estruturas
no escopo global, de repente você poderá atribuir o campob
e ficar completamente bem, sem nunca ter comportamentos indefinidos ao acessar variáveis não inicializadas.Claro, então você tem a confusão de variáveis no escopo global....
A pergunta afirma que
é um comportamento indefinido.
Isto é errado. O
x
tem um "valor indeterminado", mas não é o mesmo que comportamento indefinido.O padrão permite representações de armadilhas, mas isso é muito raro em
int
processadores convencionais, então vamos esquecer as armadilhas.Nesse caso
y
obterá "algum valor" e o manterá enquanto o acessox
poderá produzir (de acordo com o padrão) resultados diferentes sempre que for acessado.Exatamente o mesmo se aplica ao
struct
. Nenhum comportamento indefinido. Apenas valores indeterminados. Retornar a estrutura ou atribuir a outra estrutura não fará nenhuma diferença. O objeto está lá... você simplesmente não sabe o que o acessos.b
lhe dará.Não creio que o Comitê alguma vez tenha considerado essa questão, ou que os membros tivessem um entendimento consensual sobre como respondê-la. Um ponto fraco do Padrão é que seu modelo de abstração não consegue lidar de forma útil com situações em que as otimizações podem afetar o comportamento do programa de maneiras que podem ser irrelevantes para os requisitos da aplicação. Considere, por exemplo:
Se nada no universo se importasse com o conteúdo de dat[2..31] nas estruturas que são gravadas no arquivo, a maneira mais eficiente de processar o código seria simplesmente escrever as partes iniciais de
test
ex
,y
deixando o resto segurando tudo o que seguravam antes da ligação. Não creio que houvesse consenso entre os membros do Comitê de que tal otimização fosse proibida, mas não há como permitir sem caracterizar atest()
função acima. Por outro lado, se a única maneira de um programador garantir algo sobre o comportamento do programa seria garantir que todas as partes dotemp
foram escritas antes de copiá-lo parax
ey
, então a "otimização" que servia apenas para justificar o tratamentotest1()
já que invocar UB seria discutível e inútil, significando assim que não haveria razão para não tratar partes não escritas detemp.arr
como contendo valores não especificados (o que resultaria emx.arr
conteúdoy.arr
idêntico).