Acessando o membro da união inativa e o comportamento indefinido?

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

Então, é um comportamento indefinido?

A confusão é que C permite explicitamente a punção de tipos através de uma união, enquanto C ++ ( c ++ 11 ) não possui tal permissão.

c11

6.5.2.3 Estrutura e membros do sindicato

95) Se o membro utilizado para ler o conteúdo de um object de união não é o mesmo que o último membro usado para armazenar um valor no object, a parte apropriada da representação de object do valor é reinterpretada como uma representação de object no novo digite como descrito em 6.2.6 (um processo às vezes chamado de ” type punning ”). Isso pode ser uma representação de armadilha.

A situação com o C ++:

c ++ 11

9.5 Uniões [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.

C ++ posteriormente possui linguagem que permite o uso de uniões contendo struct com seqüências iniciais comuns; isso, no entanto, não permite a troca de palavras.

Para determinar se o puncionamento de tipo de união é permitido em C ++, temos que procurar mais. Lembre-se de que c99 é uma referência normativa para o C ++ 11 (e o C99 tem linguagem semelhante ao C11 permitindo a união de tipo de união):

3.9 Tipos [basic.types]

4 – A representação de object de um object do tipo T é a seqüência de N caracteres não assinados, ocupados pelo object do tipo T, onde N é igual a sizeof (T). A representação do valor de um object é 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 object que determina um valor, que é um elemento discreto de uma implementação. conjunto definido de valores. 42
42) A intenção é que o modelo de memory do C ++ seja compatível com o da Linguagem de Programação C. / ISO 9899.

Fica particularmente interessante quando lemos

3.8 Vida útil do object [basic.life]

O tempo de vida de um object do tipo T começa quando: – o armazenamento com o alinhamento e o tamanho apropriados para o tipo T é obtido e – se o object tiver boot não-trivial, sua boot está completa.

Assim, para um tipo primitivo (que ipso facto tem boot trivial) contido em uma união, a vida útil do object abrange pelo menos a vida útil da própria união. Isso nos permite invocar

3.9.2 Tipos compostos [basic.compound]

Se um object do tipo T está localizado em um endereço A, um ponteiro do tipo c * T * cujo valor é o endereço A é dito para apontar para esse object, independentemente de como o valor foi obtido.

Assumindo que a operação em que estamos interessados ​​é uma punição de tipo, ou seja, tomando o valor de um membro da união não ativo, e dado pelo acima que temos uma referência válida ao object referido por esse membro, essa operação é lvalue-to conversão -value:

4.1 Conversão Lvalue-para-rvalue [conv.lval]

Um glvalue de um tipo T não-array e não-function pode ser convertido em um prvalor. Se T é um tipo incompleto, um programa que necessite dessa conversão é mal formado. Se o object ao qual o glvalue se refere não for um object do tipo T e não for um object de um tipo derivado de T , ou se o object não for inicializado, um programa que necessite dessa conversão terá um comportamento indefinido.

A questão, então, é se um object que é um membro da união não ativo é inicializado pelo armazenamento para o membro da união ativa. Tanto quanto eu posso dizer, este não é o caso e assim embora se:

  • uma união é copiada para o armazenamento de matriz de char e vice-versa (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 de idioma por um elemento de programa em conformidade com ISO / IEC 9899 (até onde isso é definido) (3.9: 4 nota 42), então

o access a uma união por um membro não ativo é definido e é definido para seguir a representação de object e valor, o access sem uma das interposições acima é um comportamento indefinido. Isso tem implicações para as otimizações permitidas a serem executadas em tal programa, já que a implementação pode, naturalmente, presumir que o comportamento indefinido não ocorre.

Ou seja, embora possamos legitimamente formar um lvalue para um membro do sindicato não ativo (é por isso que atribuir a um membro não ativo sem construção é ok) ele é considerado não inicializado.

A norma C ++ 11 diz assim

9,5 Uniões

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? Apenas não está lá.


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

  • Um membro de um object union é acessado usando um membro de um tipo diferente (C90 6.3.2.3).

Os bytes relevantes da representação do object são tratados como um object do tipo usado para o access. Veja Type-punning. Isso pode ser uma representação de armadilha.

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


2016-01-05: Através dos comentários eu estava ligado ao C99 Defect Report # 283, que adiciona um texto semelhante como uma nota de rodapé ao documento padrão C:

78a) Se o membro usado para acessar o conteúdo de um object de união não é o mesmo que o membro usado pela última vez para armazenar um valor no object, a parte apropriada da representação de object do valor é reinterpretada como uma representação de object no novo digite como descrito em 6.2.6 (um processo às vezes chamado de “type punning”). Isso pode ser uma representação de armadilha.

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

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

Uma garantia especial é feita para simplificar o uso de sindicatos: se um sindicato contém várias estruturas que compartilham uma seqüência inicial comum (veja abaixo), e se o object union atualmente contém uma dessas estruturas, é permitido inspecionar o common parte inicial de qualquer um deles em qualquer lugar que uma declaração do tipo completo da união é visível. Duas estruturas compartilham uma seqüência inicial comum se os membros correspondentes tiverem tipos compatíveis (e, para campos de bits, as mesmas larguras) para uma seqüê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 standard contiver duas ou mais estruturas de layout standard que compartilham uma seqüência inicial comum, e se o object união de layout padrão contiver atualmente uma dessas estruturas de layout padrão, é permitido inspecionar a parte inicial comum de qualquer deles. Duas estruturas de layout padrão compartilham uma seqüência inicial comum se os membros correspondentes tiverem tipos compatíveis com layout e nenhum membro for um campo de bits ou ambos forem campos de bits com a mesma largura para uma sequência de um ou mais membros iniciais.

Embora nenhum dos dois afirme diretamente, ambos têm uma forte implicação de que “inspecionar” (ler) um membro é “permitido” somente se 1) for (parte de) o membro mais recentemente escrito, ou 2) for parte de uma inicial comum seqüência.

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

Algo que ainda não foi 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 object com tipo de união pode conter apenas um membro por vez.

Este requisito parece indicar 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.

Eu bem explico isso com um exemplo.
suponha que tenhamos 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ê escreve union A a = {10} que bem cria uma nova var do tipo A em colocar nela o valor 10.

sua memory deve ficar assim: (lembre-se de que todos os membros do sindicato obtêm o 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 acontece se eu fizer isso?

 ay[0] = 37; 

nossa memory ficará assim:

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

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

Agora, se o seu sindicato tiver um float, ou double, seu mapa de memory será mais uma bagunça, devido à forma como você armazena os números exatos. mais informações você poderia entrar aqui .