Pergunta

Já usei sindicatos anteriormente confortavelmente;hoje fiquei alarmado quando li esta postagem e descobri 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,ler de um membro do sindicato diferente daquele para quem escreveu recentemente leva a um comportamento indefinido.Se este não é o uso pretendido dos sindicatos, o que é?Alguém pode explicar detalhadamente?

Atualizar:

Eu queria esclarecer algumas coisas em retrospectiva.

  • A resposta à pergunta não é a mesma para C e C++;meu eu mais jovem e ignorante o rotulou como C e C++.
  • Depois de examinar o padrão do C++ 11, não consegui dizer conclusivamente que ele exige que o acesso/inspeção de um membro do sindicato não ativo seja indefinido/não especificado/definido pela implementação.Tudo que consegui encontrar foi §9.5/1:

    Se uma união de layout padrão contém 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 contém uma das estruturas de layout padrão, é permitido inspecionar a sequência inicial comum de qualquer de membros da 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 layout e nenhum dos membros 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.

  • Enquanto em C, (C99 TC3 - DR 283 em diante) é legal fazê-lo (obrigado a Pascal Cuoq por trazer isso à tona).Contudo, tentar fazer ainda pode levar a um comportamento indefinido, se o valor lido for inválido (chamado de "representação de trap") para o tipo pelo qual ele é lido.Caso contrário, o valor lido será definido pela implementação.
  • C89/90 chamou isso de comportamento não especificado (Anexo J) e o livro de K&R diz que sua implementação é definida.Citação de K&R:

    Este é o propósito de uma união – uma única variável que pode conter legitimamente qualquer um dos vários tipos.[...] desde que o uso seja consistente:o tipo recuperado deve ser o tipo armazenado mais recentemente.É responsabilidade do programador controlar 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.

  • Extraído do TC++PL de Stroustrup (ênfase minha)

    O uso de sindicatos pode ser essencial para compatibilidade de dados [...] às vezes mal utilizado para "conversão de tipo".

Acima de tudo, esta questão (cujo título permanece inalterado desde a minha pergunta) foi colocada com a intenção de compreender o propósito dos sindicatos E não sobre o que a norma permite Por exemplo.Usar herança para reutilização de código é, obviamente, permitido pelo padrão C++, mas não era o propósito ou a intenção original de introduzir a herança como um recurso da linguagem C++.Esta é a razão pela qual a resposta de Andrey continua sendo aceita.

Foi útil?

Solução

O objetivo dos sindicatos é bastante óbvio, mas, por algum motivo, as pessoas sentem falta com bastante frequência.

O objetivo da união é Para salvar a memória Usando a mesma região de memória para armazenar objetos diferentes em momentos diferentes. É isso.

É como uma sala em um hotel. Pessoas diferentes vivem nele por períodos de não sobreposição. Essas pessoas nunca se encontram e geralmente não sabem nada um sobre o outro. Ao gerenciar adequadamente o compartilhamento de tempo dos quartos (ou seja, certificando-se de que pessoas diferentes não sejam atribuídas a uma sala ao mesmo tempo), um hotel relativamente pequeno pode fornecer acomodações a um número relativamente grande de pessoas, e é o que os hotéis são para.

É exatamente isso que a Union faz. Se você souber que vários objetos em seu programa mantêm valores com o valor que não é de sobrecarregar as vezes, poderá "mesclar" esses objetos em um sindicato e, assim, salvar 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" a cada momento do horário do programa. Somente o membro "ativo" pode ser lido. Ao escrever para outro membro, você muda o status "ativo" para esse outro membro.

Por alguma razão, esse objetivo original do sindicato foi "substituído" com algo completamente diferente: escrever um membro de um sindicato e depois inspecioná -lo através de outro membro. Este tipo de reinterpretação de memória (também conhecido como "tipo punindo") é não é um uso válido de sindicatos. Geralmente leva a comportamentos indefinidos é descrito como produzindo comportamento definido pela implementação em C89/90.

EDITAR: Usando sindicatos para fins de tipo punindo (ou seja, escrever um membro e depois ler outro) recebeu uma definição mais detalhada em uma das corrigendas técnicas do padrão C99 (ver Dr#257 e DR#283). No entanto, lembre -se de que, formalmente, isso não o protege de encontrar um comportamento indefinido, tentando ler uma representação de armadilhas.

Outras dicas

Você pode usar sindicatos para criar estruturas como as seguintes, que contém um campo que nos diz qual componente da união é realmente usado:

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;

O comportamento é indefinido do ponto de vista da linguagem. Considere que diferentes plataformas podem ter restrições diferentes no alinhamento da memória e no endianness. O código em uma grande máquina Endian versus uma pequena Endian atualizará os valores na estrutura de maneira diferente. Corrigir o comportamento no idioma exigiria que todas as implementações usassem a mesma endianness (e restrições de alinhamento de memória ...) limitando o uso.

Se você estiver usando C ++ (está usando duas tags) e realmente se preocupa com a portabilidade, pode simplesmente usar a estrutura e fornecer um setter que leva o uint32_t e define os campos adequadamente através de operações de bitmask. O mesmo pode ser feito em C com uma função.

Editar: Eu esperava que o Aprogrammer anote uma resposta para votar e fechar este. 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 alias às quais o Aprogrammer se refere implicitamente é um ponto importante aqui. O compilador pode fazer suposições sobre a modificação (ou falta de modificação) de variáveis. No caso do sindicato, o compilador pode reordenar as instruções e mover a leitura de cada componente de cores sobre a gravação na variável de cores.

A maioria comum uso de union Eu me deparo regularmente é aliasing.

Considere o seguinte:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}

O que isso faz? Permite acesso limpo e limpo de um Vector3f vec;membros de qualquer nome:

vec.x=vec.y=vec.z=1.f ;

ou por acesso inteiro na matriz

for( int i = 0 ; i < 3 ; i++ )
  vec.elts[i]=1.f;

Em alguns casos, acessar pelo nome é a coisa mais clara que você pode fazer. Em outros casos, especialmente quando o eixo é escolhido programaticamente, a coisa mais fácil de fazer é acessar o eixo por índice numérico - 0 para x, 1 para y e 2 para z.

Como você diz, esse é um comportamento estritamente indefinido, embora "funcione" em muitas plataformas. A verdadeira razão para o uso de sindicatos é criar registros variantes.

union A {
   int i;
   double d;
};

A a[10];    // records in "a" can be either ints or doubles 
a[0].i = 42;
a[1].d = 1.23;

Obviamente, você também precisa de algum tipo de discriminador para dizer o que a variante realmente contém. E observe que nos sindicatos C ++ não são muito utilizados porque só podem conter os tipos de pod - efetivamente aqueles sem construtores e destruidores.

Em C, era uma boa maneira de implementar algo como uma variante.

enum possibleTypes{
  eInt,
  eDouble,
  eChar
}


struct Value{

    union Value {
      int iVal_;
      double dval;
      char cVal;
    } value_;
    possibleTypes discriminator_;
} 

switch(val.discriminator_)
{
  case eInt: val.value_.iVal_; break;

Em tempos de memória litlle, essa estrutura está usando menos memória do que uma estrutura que possui todo o membro.

A propósito, C fornece

    typedef struct {
      unsigned int mantissa_low:32;      //mantissa
      unsigned int mantissa_high:20;
      unsigned int exponent:11;         //exponent
      unsigned int sign:1;
    } realVal;

para acessar valores de bits.

Embora esse seja um comportamento estritamente indefinido, na prática funcionará com praticamente qualquer compilador. É um paradigma tão amplamente utilizado que qualquer compilador que se preze precisará fazer "a coisa certa" em casos como esse. Certamente deve ser preferido sobre o tipo de tipo, que pode muito bem gerar código quebrado com alguns compiladores.

Em C ++, Variante de impulso Implemente uma versão segura do sindicato, projetada para evitar o comportamento indefinido o máximo possível.

Suas performances são idênticas ao enum + union construção (pilha alocada também etc), mas usa uma lista de modelos de tipos em vez do enum :)

O comportamento pode ser indefinido, mas isso significa que não há um "padrão". Todos os compiladores decentes oferecem #pragmas Para controlar a embalagem e o alinhamento, mas pode ter padrões diferentes. Os padrões também mudarão dependendo das configurações de otimização utilizadas.

Além disso, os sindicatos não são apenas para economizar espaço. Eles podem ajudar os compiladores modernos com o tipo punindo. Se você reinterpret_cast<> Tudo o que o compilador não pode fazer sobre o que você está fazendo. Pode ter que jogar fora o que sabe sobre o seu tipo e começar novamente (forçando uma gravação de volta à memória, que é muito ineficiente nos dias de hoje em comparação com a velocidade do relógio da CPU).

Tecnicamente é indefinido, mas na realidade mais (todos?) Compiladores tratam exatamente o mesmo que usar um reinterpret_cast De um tipo para o outro, cujo resultado é definido na implementação. Eu não perderia o sono pelo seu código atual.

Para mais um exemplo do uso real dos sindicatos, o CORBA Framework serializa objetos usando a abordagem da união marcada. Todas as classes definidas pelo usuário são membros de uma (enorme) união e um Identificador inteiro diz ao Demarshaller como interpretar a União.

Outros mencionaram as diferenças de arquitetura (little - big endian).

Li o problema que como a memória das variáveis ​​é compartilhada, ao escrever em uma, as outras mudam e, dependendo do tipo, o valor pode ficar sem sentido.

por exemplo.união {float f;int eu;}x;

Escrever para x.i não faria sentido se você lesse x.f - a menos que fosse isso que você pretendia para observar os componentes de sinal, expoente ou mantissa do float.

Acho que também há uma questão de alinhamento:Se algumas variáveis ​​​​devem ser alinhadas por palavras, você poderá não obter o resultado esperado.

por exemplo.Union {char c [4];int eu;}x;

Se, hipoteticamente, em alguma máquina um char tivesse que ser alinhado por palavra, então c[0] e c[1] compartilhariam o armazenamento com i, mas não com c[2] e c[3].

No idioma C, como foi documentado em 1974, todos os membros da estrutura compartilhavam um espaço de nome comum, e o significado de "Ptr-> membro" era definiram como adicionar o deslocamento do membro a "PTR" e acessar o endereço resultante usando o tipo do membro. Esse design tornou possível usar o mesmo PTR com nomes de membros retirados de diferentes definições de estrutura, mas com o mesmo deslocamento; Os programadores usaram 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 sindicatos ao idioma tornou possível alcançar a mesma semântica que estava disponível nas versões anteriores do idioma (embora a incapacidade de ter nomes exportados para um contexto de anexo ainda possa ter necessário usar um encontro/substituição para substituir Foo-> membro em foo-> type1.Membro). O importante não era tanto que as pessoas que acrescentaram sindicatos têm algum uso de destino em particular, mas sim que fornecem um meio pelo qual os programadores que confiaram na semântica anterior, para qualquer propósito, ainda deve ser capaz de alcançar a mesma semântica, mesmo que eles tivessem que usar uma sintaxe diferente para fazê -lo.

Você pode usar AA Union por dois motivos principais:

  1. Uma maneira útil de acessar os mesmos dados de maneiras diferentes, como no seu exemplo
  2. Uma maneira de economizar espaço quando existem diferentes membros de dados dos quais apenas um pode ser 'ativo'

1 é realmente mais um hack de estilo C para o código de escrita em breve, com base no que você sabe como a arquitetura de memória do sistema de destino funciona. Como já disse, você normalmente pode se safar se não tem como alvo muitas plataformas diferentes. Acredito que alguns compiladores podem permitir que você use diretrizes de embalagem (eu sei que elas fazem em estruturas)?

Um bom exemplo de 2. pode ser encontrado no VARIANTE Tipo usado extensivamente em com.

Como outros mencionaram, os sindicatos combinados com enumerações e agrupados em estruturas podem ser usados ​​para implementar sindicatos marcados.Um uso prático é implementar o Rust's Result<T, E>, que é originalmente implementado usando um puro enum (Rust pode conter dados adicionais em variantes de enumeração).Aqui está um exemplo em C++:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top