Wcześniej korzystałem ze związków zawodowych; dzisiaj byłem zaniepokojony, gdy przeczytałem ten post i dowiedziałem się, że ten kod
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
jest w rzeczywistości nieokreślonym zachowaniem, to znaczy czytanie od członka związku innego niż ten, do którego ostatnio napisano, prowadzi do nieokreślonego zachowania. Jeśli nie jest to zamierzone użycie związków, co to jest? Czy ktoś może wyjaśnić to szczegółowo?
Aktualizacja:
Chciałem wyjaśnić kilka rzeczy z perspektywy czasu.
- Odpowiedź na pytanie nie jest taka sama dla C i C ++; moje nieświadome młodsze ja oznaczyło to jako C i C ++.
- Po przejrzeniu standardu C ++ 11 nie mogłem definitywnie stwierdzić, że wywołuje on dostęp / inspekcję nieaktywnego członka związku jest niezdefiniowany / nieokreślony / zdefiniowany w implementacji. Wszystko, co mogłem znaleźć, to §9.5 / 1:
Jeśli unia układu standardowego zawiera kilka struktur o układzie standardowym, które mają wspólną sekwencję początkową i jeśli obiekt tego typu ułożenia standardowego układu zawiera jedną ze struktur układu standardowego, dozwolone jest sprawdzenie wspólnej sekwencji początkowej dowolnej elementów struktury o standardowym układzie. § 9.2 / 19: Dwie struktury o standardowym układzie mają wspólną sekwencję początkową, jeśli odpowiadające elementy mają typy zgodne z układem i żaden z nich nie jest polem bitowym lub oba są polami bitowymi o tej samej szerokości dla sekwencji jednego lub więcej początkowych członkowie.
- Będąc w C, ( C99 TC3 - DR 283 i nowsze) jest to legalne ( podziękowania dla Pascala Cuoqa za podniesienie tego). Jednak próba zrobienia tego może nadal prowadzić do nieokreślonego zachowania , jeśli odczytana wartość okazuje się być niepoprawna (tak zwana „reprezentacja pułapki”) dla typu, przez który jest czytana. W przeciwnym razie odczytana wartość jest zdefiniowana jako implementacja.
C89 / 90 wywołało to w nieokreślonym zachowaniu (załącznik J), a książka K&R mówi, że jego implementacja jest zdefiniowana. Cytat z K&R:
Taki jest cel unii - pojedynczej zmiennej, która może zgodnie z prawem posiadać dowolny z kilku typów. [...] tak długo, jak użytkowanie jest spójne: pobrany typ musi być typem ostatnio zapisanym. Obowiązkiem programisty jest śledzenie, który typ jest obecnie przechowywany w związku; wyniki zależą od implementacji, jeśli coś jest przechowywane jako jeden typ i wyodrębniane jako inny.
Wyciąg z TC ++ PL Stroustrupa (moje wyróżnienie)
Użycie związków może być kluczowe dla zgodności danych [...] czasami niewłaściwie wykorzystywanych do „konwersji typu ”.
Przede wszystkim to pytanie (którego tytuł pozostaje niezmieniony od mojego pytania) zostało postawione z zamiarem zrozumienia celu związków ORAZ nie na temat tego, co pozwala standard. Np. Używanie dziedziczenia do ponownego użycia kodu jest oczywiście dozwolone przez standard C ++, ale nie było to celem ani pierwotną intencją wprowadzenia dziedziczenia jako funkcji języka C ++ . To jest powód, dla którego odpowiedź Andreya pozostaje nadal akceptowana.
źródło
b, g, r,
ia
może nie być ciągły, a tym samym nie pasować do układu auint32_t
. Jest to dodatek do problemów Endianess, na które zwracali uwagę inni.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
...naprawdę? cytujesz notatkę o wyjątku , a nie główny punkt na początku akapitu : „W związku, co najwyżej jeden z niestatycznych elementów danych może być aktywny w dowolnym momencie, to znaczy wartość co najwyżej jednego z elementy danych niestatycznych mogą być przechowywane w unii w dowolnym momencie. ” - i do p4: „Ogólnie rzecz biorąc, należy użyć jawnych wywołań destruktora i umieszczać nowych operatorów, aby zmienić aktywnego członka związku ”Odpowiedzi:
Cel związków jest raczej oczywisty, ale z jakiegoś powodu ludzie dość często tęsknią.
Celem unii jest oszczędzanie pamięci przy użyciu tego samego regionu pamięci do przechowywania różnych obiektów w różnych momentach. Otóż to.
To jest jak pokój w hotelu. Różni ludzie żyją w nim przez nie nakładające się okresy. Ci ludzie nigdy się nie spotykają i na ogół nic o sobie nie wiedzą. Stosownie zarządzając podziałem czasu między pokojami (tj. Upewniając się, że różne osoby nie zostaną przypisane do jednego pokoju w tym samym czasie), stosunkowo niewielki hotel może zapewnić zakwaterowanie stosunkowo dużej liczbie osób, a to właśnie hotele są dla.
To właśnie robi związek. Jeśli wiesz, że kilka obiektów w twoim programie przechowuje wartości z nie nakładającymi się okresami istnienia wartości, możesz „scalić” te obiekty w jedność, a tym samym zaoszczędzić pamięć. Podobnie jak pokój hotelowy ma co najwyżej jednego „aktywnego” najemcę w każdym momencie, związek ma co najwyżej jednego „aktywnego” członka w każdym momencie programu. Można odczytać tylko „aktywnego” członka. Pisząc do innego członka, przełączasz status „aktywnego” na tego innego członka.
Z jakiegoś powodu ten pierwotny cel związku został „zastąpiony” czymś zupełnie innym: napisaniem jednego członka związku, a następnie sprawdzeniem go za pośrednictwem innego członka. Ten rodzaj reinterpretacji pamięci (aka „typ punning”)
nie jest prawidłowym zastosowaniem związków. Generalnie prowadzi to do niezdefiniowanego zachowaniaopisanego w C89 / 90 jako wytwarzające zachowanie zdefiniowane w ramach implementacji.EDYCJA: Używanie związków do celów znakowania punktowego (tj. Pisanie jednego członka, a następnie czytanie innego) otrzymało bardziej szczegółową definicję w jednej z Corrigenda Techniczna do standardu C99 (patrz DR # 257 i DR # 283 ). Należy jednak pamiętać, że formalnie nie chroni to przed wpadnięciem w niezdefiniowane zachowanie poprzez próbę odczytania reprezentacji pułapki.
źródło
<time.h>
Windows i Unix. Odrzucenie go jako „niepoprawnego” i „niezdefiniowanego” nie jest naprawdę wystarczające, jeśli mam zostać wezwany do zrozumienia kodu, który działa w ten właśnie sposób.Możesz użyć związków do tworzenia struktur takich jak poniżej, które zawierają pole, które mówi nam, który składnik związku jest rzeczywiście używany:
źródło
int
lubchar*
na 10 przedmiotów []; w takim przypadku mogę faktycznie zadeklarować osobne struktury dla każdego typu danych zamiast VAROBJECT? Czy nie zmniejszyłoby bałaganu i nie zajmowałoby mniej miejsca?Zachowanie jest niezdefiniowane z punktu widzenia języka. Weź pod uwagę, że różne platformy mogą mieć różne ograniczenia w zakresie wyrównania pamięci i endianizmu. Kod w dużym endianie w porównaniu z małą maszyną endian zaktualizuje wartości w strukturze inaczej. Naprawienie zachowania w języku wymagałoby użycia przez wszystkie implementacje tego samego endianizmu (i ograniczeń wyrównania pamięci ...) ograniczających użycie.
Jeśli korzystasz z C ++ (używasz dwóch tagów) i naprawdę zależy Ci na przenośności, możesz po prostu użyć struktury i dostarczyć ustawiającego, który pobiera
uint32_t
i ustawia pola poprzez operacje maski bitowej. To samo można zrobić w C z funkcją.Edycja : Oczekiwałem, że AProgrammer napisze odpowiedź na głosowanie i ją zamknie. Jak zauważyły niektóre komentarze, kwestia endianizmu jest rozpatrywana w innych częściach standardu, pozwalając każdej implementacji decydować o tym, co należy zrobić, a wyrównanie i dopełnienie można również traktować inaczej. Teraz ważne są tutaj ścisłe zasady aliasingu, do których pośrednio odwołuje się AProgrammer. Kompilator może przyjmować założenia dotyczące modyfikacji (lub braku modyfikacji) zmiennych. W przypadku unii kompilator może zmienić kolejność instrukcji i przenieść odczyt każdego składnika koloru nad zapisem do zmiennej koloru.
źródło
Najbardziej powszechne stosowanie
union
regularnie natknąć się aliasing .Rozważ następujące:
Co to robi? Umożliwia czysty, schludny dostęp do
Vector3f vec;
członków według dowolnej nazwy:lub przez całkowity dostęp do tablicy
W niektórych przypadkach dostęp do nazwy jest najwyraźniejszą rzeczą, jaką możesz zrobić. W innych przypadkach, szczególnie gdy oś jest wybierana programowo, łatwiej jest uzyskać dostęp do osi za pomocą indeksu numerycznego - 0 dla x, 1 dla y i 2 dla z.
źródło
type-punning
co jest również wspomniane w pytaniu. Również przykład w pytaniu pokazuje podobny przykład.Jak mówisz, jest to ściśle niezdefiniowane zachowanie, choć będzie „działać” na wielu platformach. Prawdziwym powodem korzystania ze związków jest tworzenie rekordów wariantów.
Oczywiście potrzebujesz także pewnego rodzaju dyskryminatora, aby powiedzieć, co faktycznie zawiera wariant. I zauważ, że w związkach C ++ nie ma większego zastosowania, ponieważ mogą one zawierać tylko typy POD - skutecznie te bez konstruktorów i destruktorów.
źródło
W C był to dobry sposób na implementację czegoś w rodzaju wariantu.
W czasach małej pamięci struktura ta zużywa mniej pamięci niż struktura, która ma cały element.
Nawiasem mówiąc, C zapewnia
aby uzyskać dostęp do wartości bitów.
źródło
Chociaż jest to ściśle niezdefiniowane zachowanie, w praktyce będzie działać z praktycznie każdym kompilatorem. Jest to tak szeroko stosowany paradygmat, że każdy szanujący się kompilator będzie musiał zrobić „właściwą rzecz” w takich przypadkach. Jest to z pewnością lepsze niż pisanie na maszynie, które może generować uszkodzony kod w niektórych kompilatorach.
źródło
W C ++, Boost Variant implementuje bezpieczną wersję unii, zaprojektowaną tak, aby w jak największym stopniu zapobiegać niezdefiniowanemu zachowaniu.
Jego wydajność jest identyczna z
enum + union
konstrukcją (stos przydzielony itp.), Ale używa listy typów szablonów zamiastenum
:)źródło
Zachowanie może być niezdefiniowane, ale to tylko oznacza, że nie ma „standardu”. Wszystkie przyzwoite kompilatory oferują #pragmy do kontrolowania pakowania i wyrównania, ale mogą mieć różne ustawienia domyślne. Wartości domyślne zmienią się również w zależności od zastosowanych ustawień optymalizacji.
Związki służą nie tylko do oszczędzania miejsca. Pomagają nowoczesnym kompilatorom w pisowni czcionek. Jeśli
reinterpret_cast<>
wszystko, kompilator nie może przyjąć założeń na temat tego, co robisz. Być może będzie musiał wyrzucić to, co wie o twoim typie i zacząć od nowa (wymuszając zapis do pamięci, co jest obecnie bardzo nieefektywne w porównaniu do szybkości zegara procesora).źródło
Technicznie jest niezdefiniowany, ale w rzeczywistości większość (wszystkich?) Kompilatorów traktuje go dokładnie tak samo, jak używając
reinterpret_cast
jednego typu do drugiego, w wyniku czego zdefiniowano implementację. Nie przespałbym twojego aktualnego kodu.źródło
Dla jeszcze jednego przykładu faktycznego użycia związków, struktura CORBA serializuje obiekty przy użyciu oznaczonego związku. Wszystkie klasy zdefiniowane przez użytkownika są członkami jednego (ogromnego) związku, a identyfikator liczby całkowitej mówi demarshallerowi, jak interpretować związek.
źródło
Inni wspominali o różnicach w architekturze (mało - duży endian).
Przeczytałem problem polegający na tym, że ponieważ pamięć zmiennych jest współdzielona, to pisząc do jednej, inne się zmieniają i, w zależności od ich typu, wartość może być bez znaczenia.
na przykład. union {float f; int i; } x;
Pisanie do XI nie miałoby sensu, gdybyś czytał z XF - chyba że to jest to, co zamierzałeś, aby spojrzeć na znak, wykładnik lub mantysę elementów pływaka.
Myślę, że istnieje również problem z wyrównaniem: jeśli niektóre zmienne muszą być wyrównane ze słowami, możesz nie uzyskać oczekiwanego rezultatu.
na przykład. unia {char c [4]; int i; } x;
Gdyby hipotetycznie na jakimś komputerze znak musiał zostać wyrównany ze słowem, wówczas c [0] i c [1] współdzieli pamięć z i, ale nie c [2] i c [3].
źródło
memcpy()
jednego z drugiego. Niektóre systemy mogą spekulacyjnie wyrównywaćchar[]
przydziały, które występują poza strukturami / związkami z tego i innych powodów. W zachowanym przykładzie założenie,i
które nakłada się na wszystkie elementy elementu,c[]
jest nieprzenośne, ale dzieje się tak, ponieważ nie ma na to gwarancjisizeof(int)==4
.W języku C, jak to zostało udokumentowane w 1974 r., Wszyscy członkowie struktury mieli wspólną przestrzeń nazw i zdefiniowano znaczenie „ptr-> członek” jako dodanie przesunięcia elementu do „ptr” i uzyskanie dostępu do adresu wynikowego za pomocą typu członka. Ten projekt umożliwił użycie tego samego ptr z nazwami prętów zaczerpniętymi z różnych definicji struktur, ale z tym samym przesunięciem; programiści używali tej zdolności do różnych celów.
Kiedy członom struktury przypisano własne przestrzenie nazw, niemożliwe stało się zadeklarowanie dwóch elementów struktury z tym samym przesunięciem. Dodanie związków do języka umożliwiło uzyskanie tej samej semantyki, która była dostępna we wcześniejszych wersjach języka (chociaż niemożność wyeksportowania nazw do otaczającego kontekstu mogła nadal wymagać użycia find / replace w celu zastąpienia elementu foo-> do foo-> type1.member). Ważne było nie tyle to, że ludzie, którzy dodali związki, mieli na uwadze jakieś szczególne przeznaczenie, ale raczej to, że zapewniają środki, dzięki którym programiści, którzy w każdym celu polegali na wcześniejszej semantyce , powinni nadal być w stanie osiągnąć ta sama semantyka, nawet jeśli musieliby do tego użyć innej składni.
źródło
Możesz użyć związku z dwóch głównych powodów:
1 To naprawdę bardziej hack w stylu C do pisania skrótów na podstawie wiesz, jak działa architektura pamięci systemu docelowego. Jak już wspomniano, normalnie można go uciec, jeśli w rzeczywistości nie atakuje się wielu różnych platform. Wierzę, że niektóre kompilatory mogą pozwolić ci również używać dyrektyw pakowania (wiem, że robią to na strukturach)?
Dobry przykład 2. można znaleźć w typie VARIANT szeroko stosowanym w COM.
źródło
Jak wspomniano inni, związki w połączeniu z wyliczeniami i owinięte w struktury mogą być użyte do implementacji oznaczonych związków. Jednym praktycznym zastosowaniem jest implementacja Rust'a
Result<T, E>
, który pierwotnie jest implementowany przy użyciu czystegoenum
(Rust może przechowywać dodatkowe dane w wariantach wyliczania). Oto przykład w C ++:źródło