Trovo i sindacati C ++ piuttosto interessanti. Sembra che le persone di solito pensino solo al caso d'uso in cui si vuole cambiare il valore di un'istanza sindacale "sul posto" (che, a quanto pare, serve solo per risparmiare memoria o eseguire conversioni dubbie).
In effetti, i sindacati possono essere di grande potenza come strumento di ingegneria del software, anche quando non si cambia mai il valore di un'istanza sindacale .
Caso d'uso 1: il camaleonte
Con i sindacati, è possibile raggruppare un numero di classi arbitrarie in un'unica denominazione, che non è privo di somiglianze con il caso di una classe base e delle sue classi derivate. Ciò che cambia, tuttavia, è ciò che puoi e non puoi fare con una determinata istanza sindacale:
struct Batman;
struct BaseballBat;
union Bat
{
Batman brucewayne;
BaseballBat club;
};
ReturnType1 f(void)
{
BaseballBat bb = {/* */};
Bat b;
b.club = bb;
// do something with b.club
}
ReturnType2 g(Bat& b)
{
// do something with b, but how do we know what's inside?
}
Bat returnsBat(void);
ReturnType3 h(void)
{
Bat b = returnsBat();
// do something with b, but how do we know what's inside?
}
Sembra che il programmatore debba essere certo del tipo di contenuto di una determinata istanza sindacale quando vuole usarlo. È il caso nella funzione f
sopra. Tuttavia, se una funzione dovesse ricevere un'istanza di sindacato come argomento passato, come nel caso g
precedente, non saprebbe cosa farne. Lo stesso vale per le funzioni che restituiscono un'istanza di unione, vedi h
: come fa il chiamante a sapere cosa c'è dentro?
Se un'istanza sindacale non viene mai passata come argomento o come valore di ritorno, allora è destinata ad avere una vita molto monotona, con punte di eccitazione quando il programmatore sceglie di cambiarne il contenuto:
Batman bm = {/* */};
Baseball bb = {/* */};
Bat b;
b.brucewayne = bm;
// stuff
b.club = bb;
E questo è il caso d'uso (non) più popolare dei sindacati. Un altro caso d'uso è quando un'istanza di unione arriva con qualcosa che ti dice il suo tipo.
Caso d'uso 2: "Piacere di conoscerti, io sono object
di Class
"
Supponiamo che un programmatore scelto per associare sempre un'istanza sindacale a un descrittore di tipo (lascerò a discrezione del lettore l'immaginazione di un'implementazione per uno di questi oggetti). Ciò vanifica lo scopo dell'unione stessa se ciò che il programmatore desidera è risparmiare memoria e che la dimensione del descrittore del tipo non è trascurabile rispetto a quella dell'unione. Ma supponiamo che sia cruciale che l'istanza sindacale possa essere passata come argomento o come valore di ritorno con il chiamato o il chiamante che non sa cosa c'è dentro.
Quindi il programmatore deve scrivere switch
un'istruzione di flusso di controllo per distinguere Bruce Wayne da un bastoncino di legno o qualcosa di equivalente. Non è male quando ci sono solo due tipi di contenuti nell'unione ma ovviamente l'unione non si ridimensiona più.
Caso d'uso 3:
Come gli autori di una raccomandazione per lo standard ISO C ++ lo hanno riportato nel 2008,
Molti importanti domini problematici richiedono un numero elevato di oggetti o risorse di memoria limitate. In queste situazioni la conservazione dello spazio è molto importante e un'unione è spesso un modo perfetto per farlo. In effetti, un caso d'uso comune è la situazione in cui un sindacato non cambia mai il suo membro attivo durante la sua vita. Può essere costruito, copiato e distrutto come se fosse una struttura contenente un solo membro. Un'applicazione tipica di ciò sarebbe quella di creare una raccolta eterogenea di tipi non correlati che non sono allocati dinamicamente (forse sono costruiti sul posto in una mappa o membri di un array).
E ora, un esempio, con un diagramma di classe UML:
La situazione in un inglese semplice: un oggetto di classe A può avere oggetti di qualsiasi classe tra B1, ..., Bn e al massimo uno di ogni tipo, con n un numero piuttosto grande, diciamo almeno 10.
Non vogliamo aggiungere campi (membri dei dati) ad A in questo modo:
private:
B1 b1;
.
.
.
Bn bn;
perché n potrebbe variare (potremmo voler aggiungere classi Bx al mix) e perché ciò causerebbe un disastro con i costruttori e perché gli oggetti A occuperebbero molto spazio.
Potremmo usare un contenitore stravagante di void*
puntatori agli Bx
oggetti con cast per recuperarli, ma è fugace e quindi in stile C ... ma, cosa più importante, che ci lascerebbe con la vita di molti oggetti allocati dinamicamente da gestire.
Invece, ciò che può essere fatto è questo:
union Bee
{
B1 b1;
.
.
.
Bn bn;
};
enum BeesTypes { TYPE_B1, ..., TYPE_BN };
class A
{
private:
std::unordered_map<int, Bee> data; // C++11, otherwise use std::map
public:
Bee get(int); // the implementation is obvious: get from the unordered map
};
Quindi, per ottenere il contenuto di un'istanza di unione data
, usi a.get(TYPE_B2).b2
e simili, dove si a
trova A
un'istanza di classe .
Ciò è tanto più potente in quanto i sindacati non hanno restrizioni in C ++ 11. Vedi il documento collegato sopra o questo articolo per i dettagli.