Eu já usei sindicatos confortavelmente; hoje fiquei alarmado quando li este post e soube que esse código
union ARGB
{
uint32_t colour;
struct componentsTag
{
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t a;
} components;
} pixel;
pixel.colour = 0xff040201; // ARGB::colour is the active member from now on
// somewhere down the line, without any edit to pixel
if(pixel.components.a) // accessing the non-active member ARGB::components
Na verdade, é um comportamento indefinido, ou seja, a leitura de um membro do sindicato que não seja o recentemente escrito para leva a um comportamento indefinido. Se esse não é o uso pretendido dos sindicatos, o que é? Alguém pode explicar isso de forma elaborada?
Atualizar:
Eu queria esclarecer algumas coisas em retrospectiva.
- A resposta para a pergunta não é a mesma para C e C ++; meu eu mais jovem ignorante o identificou como C e C ++.
- Após vasculhar o padrão do C ++ 11, não pude dizer conclusivamente que o acesso / inspeção de um membro do sindicato não ativo é indefinido / não especificado / definido pela implementação. Tudo o que encontrei foi §9.5 / 1:
Se uma união de layout padrão contiver várias estruturas de layout padrão que compartilham uma sequência inicial comum, e se um objeto desse tipo de união de layout padrão contiver uma das estruturas de layout padrão, será permitido inspecionar a sequência inicial comum de qualquer de membros de estrutura de layout padrão. §9.2 / 19: 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 forem campos de bits com a mesma largura para uma sequência de uma ou mais inicial membros.
- Enquanto estiver em C, ( C99 TC3 - DR 283 em diante), é legal fazê-lo ( obrigado a Pascal Cuoq por trazer isso à tona). No entanto, tentar fazê- lo ainda pode levar a um comportamento indefinido , se o valor lido for inválido (a chamada "representação de interceptação") para o tipo pelo qual é lido. Caso contrário, o valor lido é a implementação definida.
O C89 / 90 chamou isso de comportamento não especificado (Anexo J) e o livro da K&R diz que sua implementação está definida. Citação de K&R:
Esse é o objetivo de uma união - uma única variável que pode legitimamente conter qualquer um de vários tipos. [...] desde que o uso seja consistente: o tipo recuperado deve ser o tipo armazenado mais recentemente. É responsabilidade do programador acompanhar qual tipo está atualmente armazenado em uma união; os resultados dependem da implementação se algo for armazenado como um tipo e extraído como outro.
Extrato do TC ++ PL da Stroustrup (ênfase minha)
O uso de uniões pode ser essencial para a compatibilidade de dados [...] às vezes mal utilizados para "conversão de tipo ".
Acima de tudo, esta questão (cujo título permanece inalterado desde a minha ASK) foi colocada com a intenção de compreender o propósito de sindicatos e não no que o padrão permite por exemplo, usando herança para a reutilização de código é, naturalmente, permitido pelo C ++ padrão, mas não era o objetivo ou a intenção original de introduzir herança como um recurso da linguagem C ++ . Essa é a razão pela qual a resposta de Andrey continua sendo a aceita.
fonte
b, g, r,
ea
pode não ser contíguo e, portanto, não coincidir com o layout de auint32_t
. Isso é um acréscimo às questões de Endianess que outros apontaram.scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1
...realmente? você cita uma nota de exceção , não o ponto principal logo no início do parágrafo : "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 os membros de dados não estáticos podem ser armazenados em uma união a qualquer momento ". - e até a p4: "Em geral, é preciso usar chamadas explícitas de destruidores e posicionar novos operadores para alterar o membro ativo de uma união "Respostas:
O objetivo dos sindicatos é bastante óbvio, mas por alguma razão as pessoas sentem falta dele com bastante frequência.
O objetivo da união é economizar memória usando a mesma região de memória para armazenar objetos diferentes em momentos diferentes.É isso aí.
É como um quarto em um hotel. Diferentes pessoas vivem nele por períodos de tempo sem sobreposição. Essas pessoas nunca se encontram e geralmente não sabem nada uma da outra. Ao gerenciar adequadamente o tempo compartilhado dos quartos (ou seja, garantir que pessoas diferentes não sejam designadas para um quarto ao mesmo tempo), um hotel relativamente pequeno pode oferecer acomodações para um número relativamente grande de pessoas, que é o que hotéis são para.
É exatamente isso que a união faz. Se você souber que vários objetos no seu programa mantêm valores com vida útil de valor sem sobreposição, é possível "mesclar" esses objetos em uma união e, assim, economizar memória. Assim como um quarto de hotel tem no máximo um inquilino "ativo" a cada momento, um sindicato tem no máximo um membro "ativo" em cada momento do tempo do programa. Somente o membro "ativo" pode ser lido. Ao escrever em outro membro, você alterna o status "ativo" para esse outro membro.
Por alguma razão, esse propósito original do sindicato foi "anulado" por algo completamente diferente: escrever um membro de um sindicato e depois inspecioná-lo através de outro membro. Esse tipo de reinterpretação de memória (também conhecido como "punção de tipo")
nãoéum uso válido de uniões.Geralmente, leva a um comportamento indefinido queé descrito como produzindo um comportamento definido na implementação no C89 / 90.EDIT: O uso de uniões para fins de punição de tipo (ou seja, escrever um membro e depois ler outro) recebeu uma definição mais detalhada em uma das Corrigências Técnicas do padrão C99 (consulte DR # 257 e DR # 283 ). No entanto, lembre-se de que isso formalmente não o protege de comportamento indefinido, tentando ler uma representação de interceptação.
fonte
<time.h>
Windows e do Unix. Ignorá-lo como "inválido" e "indefinido" não é realmente suficiente se for necessário entender o código que funciona exatamente dessa maneira.Você pode usar uniões para criar estruturas como a seguinte, que contém um campo que nos diz qual componente da união é realmente usado:
fonte
int
ouchar*
para 10 itens de objeto []; nesse caso, posso declarar estruturas separadas para cada tipo de dados em vez de VAROBJECT? Não reduziria a desordem e usaria menos espaço?O comportamento é indefinido do ponto de vista do idioma. Considere que plataformas diferentes podem ter restrições diferentes no alinhamento e endianismo da memória. O código em uma big endian versus uma pequena máquina endian atualizará os valores na estrutura de maneira diferente. A correção do comportamento no idioma exigiria que todas as implementações usassem o mesmo endianness (e restrições de alinhamento de memória ...) limitando o uso.
Se você estiver usando C ++ (você está usando duas tags) e realmente se preocupa com portabilidade, basta usar a estrutura e fornecer um setter que aceita
uint32_t
e define os campos adequadamente por meio de operações de máscara de bit. O mesmo pode ser feito em C com uma funçãoEdit : Eu estava esperando AProgrammer escrever uma resposta para votar e fechar esta. Como alguns comentários apontaram, o endianness é tratado em outras partes do padrão, permitindo que cada implementação decida o que fazer, e o alinhamento e o preenchimento também podem ser tratados de maneira diferente. Agora, as regras estritas de apelido às quais o AProgrammer se refere implicitamente são um ponto importante aqui. O compilador pode fazer suposições sobre a modificação (ou falta de modificação) de variáveis. No caso da união, o compilador pode reordenar as instruções e mover a leitura de cada componente de cor sobre a gravação na variável de cor.
fonte
O uso mais comum de que
union
me deparo regularmente é o alias .Considere o seguinte:
O que isso faz? Permite o acesso limpo e limpo dos
Vector3f vec;
membros de um com um dos nomes:ou por acesso inteiro à matriz
Em alguns casos, acessar pelo nome é a coisa mais clara que você pode fazer. Em outros casos, especialmente quando o eixo é escolhido programaticamente, o mais fácil é acessar o eixo pelo índice numérico - 0 para x, 1 para y e 2 para z.
fonte
type-punning
e também é mencionado na pergunta. Além disso, o exemplo na pergunta mostra um exemplo semelhante.Como você diz, esse é um comportamento estritamente indefinido, embora "funcione" em muitas plataformas. A verdadeira razão para usar uniões é criar registros variantes.
Obviamente, você também precisa de algum tipo de discriminador para dizer o que a variante realmente contém. E observe que nas uniões C ++ não são muito úteis porque podem conter apenas tipos de POD - efetivamente aqueles sem construtores e destruidores.
fonte
Em C, foi uma ótima maneira de implementar algo como uma variante.
Em tempos de pouca memória, essa estrutura está usando menos memória que uma estrutura que possui todo o membro.
A propósito, C fornece
para acessar valores de bits.
fonte
Embora esse seja um comportamento estritamente indefinido, na prática ele funcionará com praticamente qualquer compilador. É um paradigma tão amplamente usado que qualquer compilador que se preze precisará fazer "a coisa certa" em casos como esse. Certamente é preferível à punção de tipo, que pode gerar código quebrado com alguns compiladores.
fonte
Em C ++, variante Boost implementa uma versão segura da união, projetada para evitar o máximo de comportamento indefinido.
Suas performances são idênticas à
enum + union
construção (pilha alocada também, etc), mas usa uma lista de modelos de tipos em vez deenum
:)fonte
O comportamento pode ser indefinido, mas isso significa apenas que não existe um "padrão". Todos os compiladores decentes oferecem #pragmas para controlar o empacotamento e o alinhamento, mas podem ter padrões diferentes. Os padrões também serão alterados dependendo das configurações de otimização usadas.
Além disso, os sindicatos não servem apenas para economizar espaço. Eles podem ajudar os compiladores modernos com punções de tipo. Se você
reinterpret_cast<>
tudo, o compilador não pode fazer suposições sobre o que está fazendo. Pode ter que jogar fora o que sabe sobre seu tipo e começar de novo (forçando uma gravação de volta à memória, o que é muito ineficiente hoje em dia em comparação à velocidade do clock da CPU).fonte
Tecnicamente, é indefinido, mas, na realidade, a maioria dos compiladores (todos?) O tratam exatamente da mesma maneira que o uso
reinterpret_cast
de um tipo para outro, cujo resultado é a implementação definida. Eu não perderia o sono com o seu código atual.fonte
Para mais um exemplo do uso real de uniões, a estrutura CORBA serializa objetos usando a abordagem de união marcada. Todas as classes definidas pelo usuário são membros de uma união (grande) e um identificador inteiro informa ao demarshaller como interpretar a união.
fonte
Outros mencionaram as diferenças de arquitetura (little - big endian).
Li o problema de que, como a memória das variáveis é compartilhada, escrevendo para uma, as outras mudam e, dependendo do tipo, o valor pode não ter sentido.
por exemplo. união {float f; int i; } x;
Escrever para xi não faria sentido se você lesse xf - a menos que fosse isso que você pretendia para observar os componentes de sinal, expoente ou mantissa do flutuador.
Acho que também há uma questão de alinhamento: se algumas variáveis precisarem ser alinhadas por palavras, talvez você não obtenha o resultado esperado.
por exemplo. união {char c [4]; int i; } x;
Se, hipoteticamente, em alguma máquina um caractere tivesse que ser alinhado por palavras, c [0] e c [1] compartilhariam armazenamento com i, mas não com c [2] e c [3].
fonte
memcpy()
de uma para outra. Alguns sistemas podem alinhar especulativamente aschar[]
alocações que ocorrem fora das estruturas / uniões por esse e outros motivos. No exemplo existente, a suposição quei
se sobrepõe a todos os elementos dec[]
não é portátil, mas é porque não há garantia dissosizeof(int)==4
.Na linguagem C, como foi documentado em 1974, todos os membros da estrutura compartilhavam um espaço para nome comum, e o significado de "ptr-> member" foi definido como adicionar o deslocamento do membro a "ptr" e acessar o endereço resultante usando o tipo de membro. Esse design tornou possível usar o mesmo ptr com nomes de membros extraídos de diferentes definições de estrutura, mas com o mesmo deslocamento; programadores usavam essa capacidade para uma variedade de propósitos.
Quando os membros da estrutura receberam seus próprios namespaces, tornou-se impossível declarar dois membros da estrutura com o mesmo deslocamento. A adição de uniões ao idioma tornou possível obter a mesma semântica disponível em versões anteriores do idioma (embora a incapacidade de exportar nomes para um contexto envolvente ainda possa ter exigido o uso de um find / replace para substituir foo-> member em foo-> type1.member). O que era importante não era tanto que as pessoas que adicionaram sindicatos tivessem em mente algum uso específico do alvo, mas, ao contrário, elas fornecem um meio pelo qual os programadores que se basearam na semântica anterior, para qualquer finalidade , ainda deveriam ser capazes de alcançar o objetivo. mesma semântica, mesmo que precisassem usar uma sintaxe diferente para fazer isso.
fonte
Você pode usar uma união por dois motivos principais:
1 É realmente mais um truque no estilo C para atalho para código de escrita com base em que você sabe como a arquitetura de memória do sistema de destino funciona. Como já foi dito, você normalmente pode se safar se não atingir muitas plataformas diferentes. Eu acredito que alguns compiladores podem permitir que você use diretivas de embalagem também (eu sei que eles usam em estruturas)?
Um bom exemplo de 2. pode ser encontrado no tipo VARIANT usado extensivamente no COM.
fonte
Como outros mencionados, uniões combinadas com enumerações e agrupadas em estruturas podem ser usadas para implementar uniões marcadas. Um uso prático é implementar o Rust
Result<T, E>
, que é implementado originalmente usando um puroenum
(o Rust pode conter dados adicionais em variantes de enumeração). Aqui está um exemplo de C ++:fonte