Objetivo das uniões em C e C ++

254

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.

legends2k
fonte
11
Em termos simples, os compiladores podem inserir preenchimento entre elementos em uma estrutura. Assim, b, g, r,e apode não ser contíguo e, portanto, não coincidir com o layout de a uint32_t. Isso é um acréscimo às questões de Endianess que outros apontaram.
Thomas Matthews
8
É exatamente por isso que você não deve marcar as perguntas C e C ++. As respostas são diferentes, mas como os respondentes nem informam para qual tag estão respondendo (eles sabem?), Você fica com lixo.
Pascal Cuoq
5
@downvoter Obrigado por não explicar, eu entendo que você quer que eu entender magicamente a sua queixa e não repeti-lo no futuro: P
legends2k
1
Em relação à intenção original de ter união , lembre-se de que o padrão C pós-data dos sindicatos C por vários anos. Uma rápida olhada no Unix V7 mostra algumas conversões de tipo por meio de uniões.
N19815
3
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 "
underscore_d

Respostas:

407

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.

Formiga
fonte
37
+1 por ser elaborado, dando um exemplo prático simples e dizendo sobre o legado dos sindicatos!
Legends2k
6
O problema que tenho com esta resposta é que a maioria dos sistemas operacionais que eu vi possui arquivos de cabeçalho que fazem exatamente isso. Por exemplo, eu já vi isso em versões antigas (anteriores a 64 bits) do <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.
TED
31
@AndreyT “Nunca foi legal usar sindicatos para punição de tipos até muito recentemente”: 2004 não é “muito recente”, especialmente considerando que apenas o C99 foi inicialmente desajeitado, parecendo tornar a punição de tipo indefinida. Na realidade, punir os tipos de união é legal em C89, legal em C11, e legal em C99 o tempo todo, embora demorou até 2004 para o comitê corrigir a redação incorreta e a liberação subseqüente do TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Pascal Cuoq
6
@ legends2k A linguagem de programação é definida por padrão. O Corrigendo Técnico 3 da norma C99 permite explicitamente a punção de tipo na nota de rodapé 82, que eu convido você a ler por si mesmo. Não é na TV que os astros do rock são entrevistados e expressam suas opiniões sobre as mudanças climáticas. A opinião de Stroustrup não tem influência sobre o que o padrão C diz.
Pascal Cuoq
6
@ legends2k " Eu sei que a opinião de qualquer indivíduo não importa e apenas o padrão " A opinião dos escritores de compiladores importa muito mais do que a "especificação" da linguagem (extremamente ruim).
curiousguy
38

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:

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

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
Erich Kitzmueller
fonte
Concordo plenamente, sem entrar no caos de comportamento indefinido, talvez este seja o melhor comportamento pretendido dos sindicatos em que consigo pensar; mas não vai desperdiçar espaço quando estou apenas usando, digamos, intou char*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?
Legends2k
3
lendas: Em alguns casos, você simplesmente não pode fazer isso. Você usa algo como VAROBJECT em C nos mesmos casos quando usa Object em Java.
Erich Kitzmueller
A estrutura de dados dos sindicatos marcados parece ser o único uso legítimo dos sindicatos, como você explica.
legends2k
Também dê um exemplo de como usar os valores.
Ciro Santilli foi executado
1
Uma parte de um exemplo do C ++ Primer pode ajudar. wandbox.org/permlink/cFSrXyG02vOSdBk2
Rick
34

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_te define os campos adequadamente por meio de operações de máscara de bit. O mesmo pode ser feito em C com uma função

Edit : 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.

David Rodríguez - dribeas
fonte
+1 para a resposta clara e simples! Concordo, quanto à portabilidade, o método que você deu no segundo parágrafo é válido; mas posso usar da maneira que coloquei na questão, se meu código estiver vinculado a uma única arquitetura (pagando o preço da capacidade de proteção), pois ele economiza 4 bytes para cada valor de pixel e economiza tempo na execução dessa função ?
Legends2k
A questão endian não força o padrão a declarar como comportamento indefinido - reinterpret_cast tem exatamente os mesmos problemas endian, mas possui um comportamento definido pela implementação.
JoeG
1
@ legends2k, o problema é que o otimizador pode assumir que um uint32_t não é modificado gravando em uint8_t e, portanto, você obtém o valor errado quando o otimizado usa essa suposição ... @Joe, o comportamento indefinido aparece assim que você acessa o ponteiro (eu sei, existem algumas exceções).
AProgrammer
1
@ legends2k / AProgrammer: O resultado de um reinterpret_cast é uma implementação definida. O uso do ponteiro retornado não resulta em comportamento indefinido, apenas no comportamento definido pela implementação. Em outras palavras, o comportamento deve ser consistente e definido, mas não é portátil.
JoeG 22/02
1
@ legends2k: qualquer otimizador decente reconhecerá operações bit a bit que selecionam um byte inteiro e geram código para ler / gravar o byte, igual ao da união, mas bem definido (e portátil). por exemplo, uint8_t getRed () const {retorna cor & 0x000000FF; } void setRed (uint8_t r) {color = (cor & ~ 0x000000FF) | r; }
Ben Voigt
22

O uso mais comum de que unionme deparo regularmente é o alias .

Considere o seguinte:

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

O que isso faz? Permite o acesso limpo e limpo dos Vector3f vec;membros de um com um dos nomes:

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

ou por acesso inteiro à 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, o mais fácil é acessar o eixo pelo índice numérico - 0 para x, 1 para y e 2 para z.

bobobobo
fonte
3
Isso também é chamado type-punninge também é mencionado na pergunta. Além disso, o exemplo na pergunta mostra um exemplo semelhante.
precisa saber é o seguinte
4
Não é tipo punição. No meu exemplo, os tipos correspondem , então não há "trocadilho", é apenas um alias.
precisa saber é o seguinte
3
Sim, mas ainda assim, de um ponto de vista absoluto do padrão de idioma, o membro gravado e lido é diferente, o que é indefinido conforme mencionado na pergunta.
precisa saber é o seguinte
3
Espero que um padrão futuro conserte que esse caso em particular seja permitido sob a regra "subsequência inicial comum". No entanto, matrizes não participam dessa regra nos termos atuais.
Ben Voigt
3
@curiousguy: Claramente, não há exigência de que os membros da estrutura sejam colocados sem preenchimento arbitrário. Se o código testar o posicionamento ou tamanho da estrutura do membro da estrutura, o código funcionará se os acessos forem feitos diretamente através da união, mas uma leitura rigorosa da Norma indicaria que assumir o endereço de um membro da união ou estrutura produz um ponteiro que não pode ser usado como um ponteiro de seu próprio tipo, mas primeiro deve ser convertido novamente em um ponteiro no tipo de anexo ou tipo de caractere. Qualquer compilador remotamente viável estenderá o idioma, fazendo mais coisas funcionarem do que ...
supercat
10

Como você diz, esse é um comportamento estritamente indefinido, embora "funcione" em muitas plataformas. A verdadeira razão para usar uniões é 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 nas uniões C ++ não são muito úteis porque podem conter apenas tipos de POD - efetivamente aqueles sem construtores e destruidores.


fonte
Você já usou assim (como na pergunta) ?? :)
legends2k
É um pouco pedante, mas não aceito "registros variantes". Ou seja, tenho certeza que eles estavam em mente, mas se eles eram uma prioridade, por que não fornecê-los? "Forneça o elemento básico, pois pode ser útil criar outras coisas também" parece intuitivamente mais provável. Especialmente considerando pelo menos mais um aplicativo que provavelmente estava em mente os registros de E / S mapeados na memória, em que os registros de entrada e saída (enquanto sobrepostos) são entidades distintas com seus próprios nomes, tipos etc.
Steve314
@ Stev314 Se esse fosse o uso que eles tinham em mente, eles poderiam ter feito com que não fosse um comportamento indefinido.
@ Neil: +1 pela primeira vez a dizer sobre o uso real sem afetar o comportamento indefinido. Eu acho que eles poderiam ter feito a implementação definida como outras operações de punição de tipo (reinterpret_cast, etc.). Mas, como eu perguntei, você já usou para punição?
Legends2k
@ Neil - o exemplo de registro mapeado na memória não é indefinido, o endian usual / etc de lado e recebe uma flag "volátil". Gravar em um endereço neste modelo não faz referência ao mesmo registro que ler o mesmo endereço. Portanto, não há problema de "o que você está lendo de volta", pois você não está lendo de volta - qualquer saída que você escreveu para esse endereço, quando você lê, está apenas lendo uma entrada independente. O único problema é garantir que você leia o lado de entrada da união e grave o lado de saída. Era comum em coisas incorporadas - provavelmente ainda é.
Steve314
8

Em C, foi uma ótima 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 pouca memória, essa estrutura está usando menos memória 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.

Totonga
fonte
Embora ambos os seus exemplos estejam perfeitamente definidos no padrão; mas, ei, usar campos de bits é certamente um código não transportável, não é?
Legends2k
Não, não é. Tanto quanto eu sei, é amplamente suportado.
Totonga
1
O suporte do compilador não se traduz em portátil. O livro C : C (portanto, C ++) não garante a ordem dos campos nas palavras-máquina, portanto, se você usá-los pelo último motivo, o programa não será apenas não portátil, mas também depende do compilador.
Legends2k
5

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.

Paul R
fonte
2
Não há um problema endian? Uma correção relativamente fácil em comparação com "indefinido", mas vale a pena levar em consideração alguns projetos.
Steve314
5

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 + unionconstrução (pilha alocada também, etc), mas usa uma lista de modelos de tipos em vez de enum:)

Matthieu M.
fonte
5

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).

usuario
fonte
4

Tecnicamente, é indefinido, mas, na realidade, a maioria dos compiladores (todos?) O tratam exatamente da mesma maneira que o uso reinterpret_castde um tipo para outro, cujo resultado é a implementação definida. Eu não perderia o sono com o seu código atual.

JoeG
fonte
" um reinterpret_cast de um tipo para outro, cujo resultado é a implementação definida. " Não, não é. As implementações não precisam defini-lo e a maioria não o define. Além disso, qual seria o comportamento definido pela implementação permitida de converter algum valor aleatório em um ponteiro?
curiousguy
4

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.

Cubbi
fonte
4

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].

philcolbourn
fonte
Um byte que precisa ser alinhado por palavras? Isso não faz sentido. Um byte não tem nenhum requisito de alinhamento, por definição.
curiousguy
Sim, eu provavelmente deveria ter usado um exemplo melhor. Obrigado.
Philcolbourn
@curiousguy: Existem muitos casos em que se pode querer que matrizes de bytes sejam alinhadas por palavras. Se alguém possui muitas matrizes de, por exemplo, 1024 bytes, e frequentemente deseja copiar uma para a outra, tê-las alinhadas por palavras pode, em muitos sistemas, dobrar a velocidade de uma memcpy()de uma para outra. Alguns sistemas podem alinhar especulativamente as char[]alocações que ocorrem fora das estruturas / uniões por esse e outros motivos. No exemplo existente, a suposição que ise sobrepõe a todos os elementos de c[]não é portátil, mas é porque não há garantia disso sizeof(int)==4.
22715
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.

supercat
fonte
Aprecie a lição de história, no entanto, com o padrão que define tal e como indefinido, o que não era o caso na era C anterior, em que o livro de K&R era o único "padrão", é preciso ter certeza de não usá-lo para qualquer finalidade e entrar na terra UB.
precisa saber é o seguinte
2
@ legends2k: Quando o Padrão foi escrito, a maioria das implementações em C tratou os sindicatos da mesma maneira, e esse tratamento foi útil. Alguns, no entanto, não o fizeram, e os autores da Norma relutaram em marcar quaisquer implementações existentes como "não conformes". Em vez disso, eles concluíram que, se os implementadores não precisassem que o Padrão lhes dissesse para fazer alguma coisa (como evidenciado pelo fato de que eles estavam fazendo isso ), deixá-lo não especificado ou indefinido simplesmente preservaria o status quo . A noção de que isso deveria tornar as coisas menos definidas do que eram antes da redação do Padrão ... #
22616
2
... parece uma inovação muito mais recente. O que é particularmente triste sobre tudo isso é que, se os criadores de compiladores direcionados a aplicativos avançados descobrissem como adicionar diretivas de otimização úteis ao idioma que a maioria dos compiladores implementou nos anos 90, em vez de estripar os recursos e as garantias suportadas apenas " "90% das implementações, o resultado seria uma linguagem que poderia executar melhor e mais confiável do que hiper-moderno C.
supercat
2

Você pode usar uma união por dois motivos principais:

  1. Uma maneira prática de acessar os mesmos dados de maneiras diferentes, como no seu exemplo
  2. Uma maneira de economizar espaço quando há membros de dados diferentes, dos quais apenas um pode estar 'ativo'

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.

Mr. Boy
fonte
2

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 puro enum(o Rust pode conter dados adicionais em variantes de enumeração). Aqui está um exemplo de 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;
}
Kotauskas
fonte