Cel związków w C i C ++

254

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.

legends2k
źródło
11
Mówiąc wprost, kompilatory mogą wstawiać wypełnienia między elementami w strukturze. Tak więc, b, g, r,i amoże nie być ciągły, a tym samym nie pasować do układu a uint32_t. Jest to dodatek do problemów Endianess, na które zwracali uwagę inni.
Thomas Matthews
8
Właśnie dlatego nie powinieneś tagować pytań C i C ++. Odpowiedzi są różne, ale ponieważ odpowiadający nawet nie mówią, na jaki znaczek odpowiadają (czy oni w ogóle wiedzą?), Dostajesz śmieci.
Pascal Cuoq,
5
@downvoter Dzięki za nie wyjaśnienie, rozumiem, że chcesz, abym magicznie zrozumiał twój problem i nie powtarzał go w przyszłości: P
legends2k
1
Odnośnie pierwotnego zamiaru zjednoczenia , pamiętaj, że standard C datuje związki C na kilka lat. Szybkie spojrzenie na Unix V7 pokazuje kilka konwersji typu przez związki.
ninjalj
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...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
underscore_d

Odpowiedzi:

407

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 zachowania opisanego 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.

Mrówka
źródło
37
+1 za opracowanie, dając prosty praktyczny przykład i mówiąc o dziedzictwie związków!
legends2k
6
Problem z tą odpowiedzią polega na tym, że większość systemów operacyjnych, które widziałem, mają pliki nagłówkowe, które wykonują tę dokładną czynność. Na przykład widziałem to w starych (wcześniejszych niż 64-bitowe) wersjach systemów <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.
TED
31
@AndreyT „Do niedawna nie było legalnie używać związków do znakowania czcionkami”: 2004 nie jest „bardzo nowy”, szczególnie biorąc pod uwagę, że tylko C99 było początkowo niezdarnie sformułowane, sprawiając, że pisanie na klawiaturze przez związki było niezdefiniowane. W rzeczywistości znakowanie literami przez związki jest legalne w C89, legalne w C11, i było legalne w C99 przez cały czas, chociaż do 2004 r. Komitet naprawił nieprawidłowe sformułowanie, a następnie wydanie TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Pascal Cuoq
6
@ legends2k Język programowania jest definiowany przez standard. Sprostowanie techniczne 3 normy C99 wyraźnie zezwala na pisanie na maszynie w przypisie 82, do którego przeczytania zapraszam. To nie jest telewizja, w której przeprowadza się wywiady z gwiazdami rocka i wyrażają swoje opinie na temat zmian klimatu. Opinia Stroustrupa ma zerowy wpływ na to, co mówi standard C.
Pascal Cuoq,
6
@ legends2k „ Wiem, że opinia każdej osoby nie ma znaczenia i tylko standard ma znaczenie ” Opinia autorów kompilatorów ma o wiele większe znaczenie niż (bardzo słaba) „specyfikacja” języka.
ciekawy,
38

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:

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

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
Erich Kitzmueller
źródło
Całkowicie się zgadzam, bez wchodzenia w chaos niezdefiniowanych zachowań, być może jest to najlepsze zamierzone zachowanie związków, jakie mogę sobie wyobrazić; ale nie marnuje miejsca, gdy tylko używam, mówię intlub char*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?
legends2k
3
legendy: W niektórych przypadkach po prostu nie można tego zrobić. Używasz czegoś takiego jak VAROBJECT w C w tych samych przypadkach, gdy używasz Object w Javie.
Erich Kitzmueller
Jak wyjaśniono, struktura danych oznaczonych związków wydaje się być jedynym uzasadnionym zastosowaniem związków.
legends2k
Podaj także przykład użycia wartości.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@CiroSantilli 新疆 改造 中心 六四 事件 法轮功 Pomocny może być fragment przykładu z C ++ Primer . wandbox.org/permlink/cFSrXyG02vOSdBk2
Rick
34

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.

David Rodríguez - dribeas
źródło
+1 za jasną i prostą odpowiedź! Zgadzam się, jeśli chodzi o przenośność, metoda podana w drugim akapicie jest dobra; ale czy mogę użyć tego, co postawiłem w pytaniu, jeśli mój kod jest powiązany z pojedynczą architekturą (płacąc cenę za ochronę), ponieważ oszczędza 4 bajty na każdą wartość piksela i oszczędza trochę czasu podczas uruchamiania tej funkcji ?
legends2k
Problem endian nie zmusza standardu do zadeklarowania go jako niezdefiniowane zachowanie - reinterpret_cast ma dokładnie takie same problemy z endianem, ale ma zachowanie zdefiniowane w implementacji.
JoeG
1
@ legends2k, problem polega na tym, że optymalizator może założyć, że uint32_t nie jest modyfikowany, pisząc do uint8_t, a więc otrzymujesz niewłaściwą wartość, gdy zoptymalizowane użycie tego założenia ... @Joe, niezdefiniowane zachowanie pojawia się, gdy tylko uzyskasz dostęp do wskaźnik (wiem, są pewne wyjątki).
AProgrammer
1
@ legends2k / AProgrammer: Rezultatem reinterpretacji jest zdefiniowana implementacja. Użycie zwróconego wskaźnika nie powoduje niezdefiniowanego zachowania, a jedynie zachowanie zdefiniowane w implementacji. Innymi słowy, zachowanie musi być spójne i zdefiniowane, ale nie jest przenośne.
JoeG
1
@ legends2k: każdy przyzwoity optymalizator rozpozna bitowe operacje, które wybierają cały bajt i wygeneruje kod do odczytu / zapisu bajtu, taki sam jak unia, ale dobrze zdefiniowany (i przenośny). np. uint8_t getRed () const {return color & 0x000000FF; } void setRed (uint8_t r) {color = (color & ~ 0x000000FF) | r; }
Ben Voigt
22

Najbardziej powszechne stosowanie unionregularnie natknąć się aliasing .

Rozważ następujące:

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

Co to robi? Umożliwia czysty, schludny dostęp do Vector3f vec;członków według dowolnej nazwy:

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

lub przez całkowity dostęp do tablicy

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

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.

Bobobobo
źródło
3
Jest to również nazywane, type-punningco jest również wspomniane w pytaniu. Również przykład w pytaniu pokazuje podobny przykład.
legends2k
4
To nie jest pisanie na klawiaturze. W moim przykładzie typy się zgadzają , więc nie ma „pun”, tylko aliasing.
bobobobo,
3
Tak, ale nadal, z absolutnego punktu widzenia standardu językowego, członek napisany do i czytający jest inny, co nie jest zdefiniowane, jak wspomniano w pytaniu.
legends2k
3
Mam nadzieję, że przyszły standard naprawi ten konkretny przypadek, który będzie dozwolony na zasadzie „wspólnej podsekwencji początkowej”. Jednak tablice nie uczestniczą w tej regule w obecnym brzmieniu.
Ben Voigt
3
@curiousguy: Oczywiście nie ma wymogu, aby elementy konstrukcji były umieszczane bez arbitralnego wypełnienia. Jeśli testuje kod w celu umieszczenia elementu lub rozmiaru struktury, kod powinien działać, jeśli dostęp odbywa się bezpośrednio przez połączenie, ale ścisłe odczytanie standardu wskazuje, że przyjęcie adresu związku lub elementu struktury daje wskaźnik, którego nie można użyć jako wskaźnik własnego typu, ale najpierw musi zostać przekonwertowany z powrotem na wskaźnik na typ otaczający lub typ znaku. Każdy zdalnie działający kompilator rozszerzy język, sprawiając, że więcej rzeczy będzie działać niż ...
supercat
10

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.

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;

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
Czy używałeś go w ten sposób (jak w pytaniu)? :)
legends2k
To trochę pedantyczne, ale nie do końca akceptuję „wersje wariantowe”. To znaczy, jestem pewien, że mieli na myśli, ale jeśli byliby priorytetem, dlaczego by ich nie zapewnić? „Podaj element konstrukcyjny, ponieważ przydatne może być również budowanie innych rzeczy”, wydaje się bardziej intuicyjnie bardziej prawdopodobne. Zwłaszcza biorąc pod uwagę co najmniej jeszcze jedną aplikację, która prawdopodobnie miała na myśli - rejestry we / wy odwzorowane w pamięci, w których rejestry wejściowe i wyjściowe (podczas nakładania się) są odrębnymi jednostkami o własnych nazwach, typach itp.
Steve314
@ Stev314 Gdyby o to chodziło, mogliby sprawić, że zachowanie nie będzie niezdefiniowane.
@Neil: +1 po raz pierwszy mówi o rzeczywistym użyciu bez uderzenia w niezdefiniowane zachowanie. Wydaje mi się, że mogliby zdefiniować implementację tak, jak inne operacje punningowe (reinterpret_cast itp.). Ale jak zapytałem, czy używałeś go do pisania na klawiaturze?
legends2k
@Neil - przykład rejestru odwzorowanego w pamięci nie jest niezdefiniowany, zwykły endian / etc na bok i ma flagę „niestabilną”. Zapisywanie adresu w tym modelu nie odnosi się do tego samego rejestru, co odczyt tego samego adresu. Dlatego nie ma problemu „co czytasz”, ponieważ nie czytasz - niezależnie od tego, co napisałeś na ten adres, kiedy czytasz, czytasz tylko niezależne dane wejściowe. Jedynym problemem jest upewnienie się, że czytasz stronę wejściową unii i piszesz stronę wyjściową. Był powszechny w osadzonych rzeczach - prawdopodobnie nadal jest.
Steve314,
8

W C był to dobry sposób na implementację czegoś w rodzaju wariantu.

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;

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

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

aby uzyskać dostęp do wartości bitów.

Totonga
źródło
Chociaż oba twoje przykłady są doskonale zdefiniowane w standardzie; ale, hej, użycie pól bitowych jest z pewnością strzałem w kod nie do przeniesienia, prawda?
legends2k
Nie, nie jest. O ile mi wiadomo, jest szeroko obsługiwany.
Totonga
1
Obsługa kompilatora nie przekłada się na przenośny. C Book : C (a tym samym C ++) nie daje żadnej gwarancji na uporządkowanie pól w słowach maszynowych, więc jeśli użyjesz ich z tego drugiego powodu, program będzie nie tylko nieprzenośny, ale będzie również zależny od kompilatora.
legends2k
5

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.

Paul R.
źródło
2
Czy nie ma problemu z endianem? Stosunkowo łatwa poprawka w porównaniu z „niezdefiniowanym”, ale jeśli tak, to warto ją wziąć pod uwagę w przypadku niektórych projektów.
Steve314
5

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 + unionkonstrukcją (stos przydzielony itp.), Ale używa listy typów szablonów zamiast enum:)

Matthieu M.
źródło
5

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

Nacięcie
źródło
4

Technicznie jest niezdefiniowany, ale w rzeczywistości większość (wszystkich?) Kompilatorów traktuje go dokładnie tak samo, jak używając reinterpret_castjednego typu do drugiego, w wyniku czego zdefiniowano implementację. Nie przespałbym twojego aktualnego kodu.

JoeG
źródło
rzut_interpretacyjny z jednego typu na drugi, w wyniku którego zdefiniowano implementację. ” Nie, nie jest. Implementacje nie muszą go definiować, a większość go nie definiuje. Ponadto, jakie byłoby dozwolone zdefiniowane wdrożenie implementujące rzutowanie jakiejś losowej wartości na wskaźnik?
ciekawy
4

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.

Cubbi
źródło
4

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

philcolbourn
źródło
Bajt, który musi być dopasowany do słów? To nie ma sensu. Bajt ma obowiązku wyrównania, z definicji.
ciekawy
Tak, prawdopodobnie powinienem był użyć lepszego przykładu. Dzięki.
philcolbourn
@curiousguy: Istnieje wiele przypadków, w których można chcieć, aby tablice bajtów były dopasowane do słów. Jeśli ktoś ma wiele tablic, np. 1024 bajtów, i często będzie chciał skopiować jeden do drugiego, wyrównanie słów może w wielu systemach podwoić prędkość 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, iktóre nakłada się na wszystkie elementy elementu, c[]jest nieprzenośne, ale dzieje się tak, ponieważ nie ma na to gwarancji sizeof(int)==4.
supercat
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.

supercat
źródło
Doceń lekcję historii, jednak ze standardem definiującym takie i takie jak niezdefiniowane, co nie miało miejsca w minionej epoce C, w której książka K&R była jedynym „standardem”, trzeba być pewnym, że nie będzie jej używać do jakichkolwiek celów i wejść do ziemi UB.
legends2k
2
@ legends2k: Kiedy napisano Standard, większość implementacji C traktowała związki w ten sam sposób i takie traktowanie było przydatne. Kilku jednak tego nie zrobiło, a autorzy Standardu nie lubili oznaczać istniejących wdrożeń jako „niezgodne”. Zamiast tego doszli do wniosku, że gdyby realizatorzy nie potrzebowali Standardu, aby im coś powiedział (o czym świadczy fakt, że już to robili ), pozostawienie go nieokreślonego lub niezdefiniowanego po prostu zachowałoby status quo . Pomysł, że powinno to uczynić rzeczy mniej zdefiniowanymi niż były przed napisaniem Standardu ...
supercat
2
... wydaje się znacznie nowszą innowacją. Szczególnie smutne w tym wszystkim jest to, że jeśli autorzy kompilatorów atakujący zaawansowane aplikacje mieliby dowiedzieć się, jak dodać użyteczne dyrektywy optymalizacyjne do języka, który większość kompilatorów zaimplementował w latach 90., zamiast patroszenia funkcji i gwarancji obsługiwanych przez „tylko „90% wdrożeń, w wyniku czego powstałby język, który mógłby działać lepiej i bardziej niezawodnie niż hipernowoczesny C.
supercat
2

Możesz użyć związku z dwóch głównych powodów:

  1. Wygodny sposób na dostęp do tych samych danych na różne sposoby, na przykład w twoim przykładzie
  2. Sposób na zaoszczędzenie miejsca, gdy istnieją różne elementy danych, z których tylko jeden może być „aktywny”

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.

Mr. Boy
źródło
2

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 czystego enum(Rust może przechowywać dodatkowe dane w wariantach wyliczania). Oto przykład w 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
źródło