Acessando membro inativo da união e comportamento indefinido?

129

Fiquei com a impressão de que acessar um unionmembro que não seja o último conjunto é UB, mas não consigo encontrar uma referência sólida (além de respostas afirmando que é UB, mas sem nenhum suporte do padrão).

Então, é um comportamento indefinido?

Luchian Grigore
fonte
3
O C99 (e também acredito que o C ++ 11) permite explicitamente punções de tipo com os sindicatos. Então, acho que se enquadra no comportamento "definido pela implementação".
Mysticial
1
Eu o usei em várias ocasiões para converter de int individual para char. Então, eu definitivamente sei que não é indefinido. Usei-o no compilador Sun CC. Portanto, ainda pode ser dependente do compilador.
Go4sri 7/07
42
@ go4sri: Claramente, você não sabe o que significa um comportamento indefinido. O fato de que ele pareceu funcionar para você em algum exemplo não contradiz sua indefinição.
Benjamin Lindley
4
Relacionados: Propósito dos Sindicatos em C e C ++
legends2k
4
@Mysticial, a postagem do blog que você vincula é muito específica em relação à C99; esta pergunta está marcada apenas para C ++.
Davmac

Respostas:

131

A confusão é que C permite explicitamente punções de tipo por meio de uma união, enquanto C ++ () não tem essa permissão.

6.5.2.3 Estrutura e membros do sindicato

95) Se o membro usado para ler o conteúdo de um objeto de união não for o mesmo que o membro usado pela última vez para armazenar um valor no objeto, a parte apropriada da representação do objeto será reinterpretada como uma representação de objeto no novo digite como descrito em 6.2.6 (um processo às vezes chamado de '' punção de tipo ''). Esta pode ser uma representação de interceptação.

A situação com C ++:

9.5 Sindicatos [class.union]

Em uma união, no máximo um dos membros de dados não estáticos pode estar ativo a qualquer momento, ou seja, o valor de no máximo um dos membros de dados não estáticos pode ser armazenado em uma união a qualquer momento.

O C ++ posteriormente possui uma linguagem que permite o uso de uniões contendo structs com seqüências iniciais comuns; no entanto, isso não permite punição de tipo.

Para determinar se a punção de tipo de união é permitida em C ++, precisamos procurar mais. Lembre-se que é uma referência normativa para C ++ 11 (e C99 possui linguagem semelhante à C11, permitindo punção de tipo de união):

3.9 Tipos [basic.types]

4 - A representação do objeto de um objeto do tipo T é a sequência de N objetos de caracteres não assinados capturados pelo objeto do tipo T, onde N é igual a sizeof (T). A representação do valor de um objeto é o conjunto de bits que contém o valor do tipo T. Para tipos trivialmente copiáveis, a representação do valor é um conjunto de bits na representação do objeto que determina um valor, que é um elemento discreto de uma implementação. conjunto de valores definido. 42
42) A intenção é que o modelo de memória do C ++ seja compatível com o da linguagem de programação C. ISO / IEC 9899

Fica particularmente interessante quando lemos

3.8 Duração do objeto [basic.life]

A vida útil de um objeto do tipo T começa quando: - é obtido o armazenamento com o alinhamento e tamanho adequados para o tipo T e - se o objeto tiver uma inicialização não trivial, sua inicialização estará concluída.

Portanto, para um tipo primitivo (cujo ipso facto possui inicialização trivial) contido em uma união, o tempo de vida do objeto abrange pelo menos o tempo de vida da própria união. Isso nos permite invocar

3.9.2 Tipos de compostos [basic.compound]

Se um objeto do tipo T estiver localizado no endereço A, diz-se que um ponteiro do tipo cv T *, cujo valor é o endereço A, aponte para esse objeto, independentemente de como o valor foi obtido.

Supondo que a operação na qual estamos interessados ​​seja punitiva, ou seja, assumindo o valor de um membro do sindicato não ativo, e dado acima, que temos uma referência válida ao objeto referido por esse membro, essa operação é lvalue-to -rvalue conversion:

4.1 Conversão de valor em valor [conv.lval]

Um glvalue de um tipo sem função e sem matriz Tpode ser convertido em um prvalor. Se Té um tipo incompleto, um programa que requer essa conversão está mal formado. Se o objeto ao qual o glvalue se refere não é um objeto do tipo Te não é um objeto de um tipo derivado T, ou se o objeto não for inicializado, um programa que necessite dessa conversão terá um comportamento indefinido.

A questão então é se um objeto que é um membro da união não ativo é inicializado por armazenamento no membro da união ativo. Tanto quanto posso dizer, este não é o caso e, portanto, se:

  • uma união é copiada no chararmazenamento e retorno da matriz (3.9: 2) ou
  • uma união é copiada em sentido inverso para outra união do mesmo tipo (3.9: 3), ou
  • uma união é acessada através dos limites do idioma por um elemento do programa em conformidade com a ISO / IEC 9899 (na medida em que seja definido) (3.9: 4, nota 42);

o acesso a uma união por um membro não ativo é definido e segue a representação de objeto e valor; o acesso sem uma das interposições acima é um comportamento indefinido. Isso tem implicações para as otimizações permitidas a serem executadas em um programa desse tipo, pois a implementação pode, é claro, presumir que um comportamento indefinido não ocorra.

Ou seja, embora possamos legitimamente formar um valor mínimo para um membro do sindicato não ativo (é por isso que atribuir a um membro não ativo sem construção é aceitável), ele é considerado não inicializado.

ecatmur
fonte
5
3.8 / 1 diz que a vida útil de um objeto termina quando seu armazenamento é reutilizado. Isso indica para mim que um membro não ativo da vida útil de uma união terminou porque seu armazenamento foi reutilizado para o membro ativo. Isso significa que você é limitado na forma como usa o membro (3.8 / 6).
bames53
2
Sob essa interpretação, todo bit de memória contém simultaneamente objetos de todos os tipos que são trivialmente inicializáveis ​​e têm alinhamento apropriado ... Assim, o tempo de vida de qualquer tipo não trivialmente inicializável é imediatamente encerrado, pois seu armazenamento é reutilizado para todos esses outros tipos ( e não reiniciar porque eles não são trivialmente inicializáveis)?
bames53
3
O texto 4.1 está completo e totalmente quebrado e desde então foi reescrito. Ele proibia todo tipo de coisas perfeitamente válidas: proibia memcpyimplementações personalizadas (acessar objetos usando unsigned charlvalues), proibia acessos a *pafter int *p = 0; const int *const *pp = &p;(mesmo que a conversão implícita de int**para const int*const*fosse válida), proibia mesmo acessar cdepois struct S s; const S &c = s;. Edição 616 do CWG . A nova redação permite isso? Há também [basic.lval].
2
@ Onipresente: Isso faria sentido, mas também precisaria esclarecer (e o Padrão C também precisa esclarecer): o que o &operador unário significa quando aplicado a um membro do sindicato. Eu acho que o ponteiro resultante deve ser utilizável para acessar o membro pelo menos até a próxima vez que o próximo uso direto ou indireto de qualquer outro valor de membro, mas no gcc o ponteiro não é utilizável por tanto tempo, o que levanta uma questão sobre o que o &operador deve querer dizer.
Supercat
4
Uma pergunta sobre "Lembre-se de que c99 é uma referência normativa para C ++ 11" Isso não é relevante apenas, onde o padrão c ++ se refere explicitamente ao padrão C (por exemplo, para as funções da biblioteca c)?
MikeMB
28

O padrão C ++ 11 diz desta maneira

9.5 Sindicatos

Em uma união, no máximo um dos membros de dados não estáticos pode estar ativo a qualquer momento, ou seja, o valor de no máximo um dos membros de dados não estáticos pode ser armazenado em uma união a qualquer momento.

Se apenas um valor é armazenado, como você pode ler outro? Simplesmente não está lá.


A documentação do gcc lista isso em Comportamento definido por implementação

  • Um membro de um objeto de união é acessado usando um membro de um tipo diferente (C90 6.3.2.3).

Os bytes relevantes da representação do objeto são tratados como um objeto do tipo usado para o acesso. Consulte Punção de tipo. Esta pode ser uma representação de interceptação.

indicando que isso não é exigido pelo padrão C.


05-01/2016: Através dos comentários, fui vinculado ao Relatório de Defeitos C99 # 283, que adiciona um texto semelhante a uma nota de rodapé ao documento padrão C:

78a) Se o membro usado para acessar o conteúdo de um objeto de união não for o mesmo que o membro usado pela última vez para armazenar um valor no objeto, a parte apropriada da representação do objeto do valor será reinterpretada como uma representação do objeto no novo digite como descrito em 6.2.6 (um processo às vezes chamado de "punção de tipo"). Esta pode ser uma representação de interceptação.

Não tenho certeza se isso esclarece muito, considerando que uma nota de rodapé não é normativa para o padrão.

Bo Persson
fonte
10
@LuchianGrigore: UB não é o que o padrão diz ser UB, é o que o padrão não descreve como deve funcionar. É exatamente esse o caso. O padrão descreve o que acontece? Diz que sua implementação está definida? Não e não. Então é UB. Além disso, em relação ao argumento "membros compartilham o mesmo endereço de memória", você terá que se referir às regras de alias, que o levarão ao UB novamente.
Yakov Galka
5
@ Luchian: É bastante claro o que significa ativo "
Benjamin Lindley
5
@LuchianGrigore: Sim, existem. Há uma quantidade infinita de casos que o padrão não trata (e não pode). (C ++ é uma VM completa de Turing, por isso está incompleta.) E daí? Ele explica o que significa "ativo", consulte a citação acima, depois de "isso é".
Yakov Galka 07/07
8
@LuchianGrigore: A omissão da definição explícita de comportamento também é um comportamento indefinido não considerado, de acordo com a seção de definições.
JXH
5
@ Claudiu Esse é o UB por um motivo diferente - viola o aliasing estrito.
Mysticial
18

Eu acho que o mais próximo que o padrão chega de dizer que é um comportamento indefinido é onde ele define o comportamento de uma união que contém uma sequência inicial comum (C99, §6.5.2.3 / 5):

Uma garantia especial é feita para simplificar o uso de uniões: se uma união contiver várias estruturas que compartilham uma sequência inicial comum (veja abaixo), e se o objeto de união atualmente contiver uma dessas estruturas, é permitido inspecionar a união comum. parte inicial de qualquer um deles em qualquer lugar em que seja visível uma declaração do tipo completo da união. Duas estruturas compartilham uma sequência inicial comum se os membros correspondentes tiverem tipos compatíveis (e, para campos de bits, as mesmas larguras) para uma sequência de um ou mais membros iniciais.

O C ++ 11 fornece requisitos / permissão semelhantes em §9.2 / 19:

Se uma união de layout padrão contiver duas ou mais estruturas de layout padrão que compartilham uma sequência inicial comum, e se o objeto de união de layout padrão contiver atualmente uma dessas estruturas de layout padrão, será permitido inspecionar a parte inicial comum de qualquer deles. Duas estruturas de layout padrão compartilham uma sequência inicial comum se os membros correspondentes tiverem tipos compatíveis com o layout e nenhum membro for um campo de bits ou ambos são campos de bits com a mesma largura para uma sequência de um ou mais membros iniciais.

Embora nenhum deles o exponha diretamente, ambos têm uma forte implicação de que "inspecionar" (ler) um membro é "permitido" apenas se 1) for (parte do) membro mais recentemente escrito ou 2) fizer parte de uma inicial comum seqüência.

Essa não é uma afirmação direta de que fazer o contrário é um comportamento indefinido, mas é o mais próximo do qual estou ciente.

Jerry Coffin
fonte
Para fazer esta completo, você precisa saber o que "tipos de layout compatível" são para C ++, ou "tipos compatíveis" são para C.
Michael Anderson
2
@ MichaelAnderson: Sim e não. Você precisa lidar com aqueles quando / se quiser ter certeza de que algo se enquadra nessa exceção - mas a verdadeira questão aqui é se algo que claramente está fora da exceção realmente dá à UB. Eu acho que isso está suficientemente implícito aqui para deixar clara a intenção, mas acho que nunca foi afirmada diretamente.
Jerry Coffin
Essa coisa de "sequência inicial comum" pode ter salvado 2 ou 3 dos meus projetos da Lixeira. Fiquei lívido quando li pela primeira vez sobre a maioria dos usos punitivos de unions sendo indefinidos, já que um blog em particular me deu a impressão de que isso era bom e construí várias estruturas e projetos grandes em torno dele. Agora acho que posso estar bem, afinal, já que meus unions contêm classes com os mesmos tipos na frente
underscore_d
@JerryCoffin, acho que você estava sugerindo a mesma pergunta que eu: e se o nosso unioncontiver, por exemplo, a uint8_te a class Something { uint8_t myByte; [...] };- eu assumiria que essa condição também se aplicaria aqui, mas é redigida com muita deliberação para permitir apenas structs. Felizmente, eu já estou usando esses em vez de primitivos brutos: O
underscore_d
@underscore_d: O padrão C cobre pelo menos uma parte dessa pergunta: "Um ponteiro para um objeto de estrutura, adequadamente convertido, aponta para seu membro inicial (ou se esse membro é um campo de bits, então para a unidade em que reside) , e vice versa."
Jerry Coffin
12

Algo que ainda não é mencionado pelas respostas disponíveis é a nota de rodapé 37 no parágrafo 21 da seção 6.2.5:

Observe que o tipo agregado não inclui o tipo de união, porque um objeto com o tipo de união pode conter apenas um membro por vez.

Esse requisito parece implicar claramente que você não deve escrever em um membro e ler em outro. Nesse caso, pode ser um comportamento indefinido por falta de especificação.

mpu
fonte
Muitas implementações documentam seus formatos de armazenamento e regras de layout. Essa especificação, em muitos casos, implicaria qual seria o efeito de ler o armazenamento de um tipo e escrever como outro na ausência de regras dizendo que os compiladores não precisam realmente usar seu formato de armazenamento definido, exceto quando as coisas são lidas e gravadas usando ponteiros de um tipo de caractere.
Supercat
-3

Eu bem explico isso com um exemplo.
suponha que temos a seguinte união:

union A{
   int x;
   short y[2];
};

Eu suponho que sizeof(int)dê 4 e que sizeof(short)dê 2.
Quando você escrever, union A a = {10}crie uma nova var do tipo A e coloque o valor 10.

sua memória deve ficar assim: (lembre-se de que todos os membros do sindicato ficam no mesmo local)

       | x
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

como você pode ver, o valor de ax é 10, o valor de ay 1 é 10 e o valor de ay [0] é 0.

agora, o que acontecerá bem se eu fizer isso?

a.y[0] = 37;

nossa memória ficará assim:

       | x
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

isso transformará o valor de ax para 2424842 (em decimal).

agora, se sua união tiver um valor flutuante ou duplo, seu mapa de memória ficará mais confuso, devido à maneira como você armazena números exatos. mais informações você pode entrar aqui .

elyashiv
fonte
18
:) Não foi isso que eu pedi. Eu sei o que acontece internamente. Eu sei que funciona. Eu perguntei se está no padrão.
Luchian Grigore 17/08/2012