Niezależnie od tego, jak „zły” jest kod i przy założeniu, że wyrównanie itp. Nie jest problemem na kompilatorze / platformie, czy jest to niezdefiniowane lub zepsute zachowanie?
Jeśli mam taką strukturę: -
struct data
{
int a, b, c;
};
struct data thing;
Czy jest legalny dostęp a
, b
i c
jak (&thing.a)[0]
, (&thing.a)[1]
i (&thing.a)[2]
?
W każdym przypadku, na każdym kompilatorze i platformie, na którym go wypróbowałem, przy każdym ustawieniu, które wypróbowałem, „działało”. Martwię się tylko, że kompilator może nie zdawać sobie sprawy, że b i rzecz [1] to to samo i zapisy do 'b' mogą zostać umieszczone w rejestrze, a rzecz [1] odczyta niewłaściwą wartość z pamięci (na przykład). Jednak w każdym przypadku robiłem to dobrze. (Zdaję sobie sprawę, oczywiście, że to niewiele dowodzi)
To nie jest mój kod; to jest kod, z którym muszę pracować, interesuje mnie, czy jest to zły kod, czy uszkodzony kod, ponieważ różnica ma duży wpływ na moje priorytety zmiany go :)
Otagowano C i C ++. Najbardziej interesuje mnie C ++, ale także C, jeśli jest inne, tylko dla zainteresowania.
Odpowiedzi:
To jest nielegalne 1 . To niezdefiniowane zachowanie w C ++.
Bierzesz członków w sposób tablicowy, ale oto, co mówi standard C ++ (moje podkreślenie):
Jednak w przypadku członków nie ma takiego ciągłego wymogu:
Chociaż powyższe dwa cudzysłowy powinny wystarczyć, aby wskazać, dlaczego indeksowanie do a
struct
nie jest zachowaniem zdefiniowanym przez standard C ++, wybierzmy jeden przykład: spójrz na wyrażenie(&thing.a)[2]
- dotyczące operatora indeksu dolnego:Zagłębiając się w pogrubiony tekst powyższego cytatu: dotyczący dodawania typu całkowitego do typu wskaźnikowego (zwróć uwagę na podkreślenie tutaj).
Zwróć uwagę na wymaganie dotyczące tablicy dla klauzuli if ; w przeciwnym razie inaczej w powyższym cytacie. Wyrażenie
(&thing.a)[2]
oczywiście nie kwalifikuje się do klauzuli if ; Stąd niezdefiniowane zachowanie.Na marginesie: chociaż intensywnie eksperymentowałem z kodem i jego odmianami na różnych kompilatorach i nie wprowadzają tutaj żadnego dopełnienia (to działa ); z punktu widzenia konserwacji kod jest wyjątkowo delikatny. przed wykonaniem tej czynności należy nadal zapewnić, że implementacja przydzieliła członków w sposób ciągły. I trzymaj się w granicach :-). Ale jego nadal niezdefiniowane zachowanie ...
Niektóre możliwe obejścia (ze zdefiniowanym zachowaniem) zostały podane w innych odpowiedziach.
Jak słusznie wskazałem w komentarzach, [basic.lval / 8] , który był w mojej poprzedniej edycji, nie ma zastosowania. Dzięki @ 2501 i @MM
1 : Zobacz odpowiedź @ Barry'ego na to pytanie dla jedynego przypadku prawnego, w którym możesz uzyskać dostęp do
thing.a
członka struktury za pośrednictwem tej strony.źródło
- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
Nie. W C jest to niezdefiniowane zachowanie, nawet jeśli nie ma dopełnienia.
To, co powoduje niezdefiniowane zachowanie, to dostęp poza granicami 1 . Kiedy masz skalar (składowe a, b, c w strukturze) i próbujesz użyć go jako tablicy 2, aby uzyskać dostęp do następnego hipotetycznego elementu, wywołujesz niezdefiniowane zachowanie, nawet jeśli zdarzy się, że w miejscu znajduje się inny obiekt tego samego typu ten adres.
Możesz jednak użyć adresu obiektu struct i obliczyć przesunięcie do określonego elementu członkowskiego:
Należy to zrobić dla każdego elementu osobno, ale można to umieścić w funkcji przypominającej dostęp do tablicy.
1 (Cytat z: ISO / IEC 9899: 201x 6.5.6 Operatory addytywne 8)
Jeżeli wynik jest wskazany o jeden za ostatnim elementem tablicy, nie powinien być używany jako operand jednoargumentowego operatora *, który jest oceniany.
2 (Cytat z: ISO / IEC 9899: 201x 6.5.6 Operatory addytywne 7)
Na potrzeby tych operatorów wskaźnik do obiektu, który nie jest elementem tablicy, zachowuje się tak samo, jak wskaźnik do pierwszego elementu tablica o długości jeden z typem obiektu jako typem elementu.
źródło
char* p = ( char* )&thing.a + offsetof( thing , b );
prowadzi to do nieokreślonego zachowania?W C ++, jeśli naprawdę tego potrzebujesz - utwórz operator []:
nie tylko gwarantuje, że będzie działać, ale jego użycie jest prostsze, nie musisz pisać nieczytelnego wyrażenia
(&thing.a)[0]
Uwaga: ta odpowiedź jest udzielana przy założeniu, że masz już strukturę z polami i musisz dodać dostęp przez indeks. Jeśli szybkość jest problemem i możesz zmienić strukturę, może to być bardziej skuteczne:
To rozwiązanie zmieniłoby rozmiar struktury, więc możesz użyć również metod:
źródło
thing.a()
.W przypadku języka c ++: Jeśli chcesz uzyskać dostęp do elementu członkowskiego bez znajomości jego nazwy, możesz użyć wskaźnika do zmiennej składowej.
źródło
offsetoff
w C.W ISO C99 / C11, punktowanie typu oparte na związkach jest legalne, więc można go używać zamiast indeksowania wskaźników do innych niż tablice (zobacz różne inne odpowiedzi).
ISO C ++ nie zezwala na punktowanie typów oparte na unii. GNU C ++ robi to jako rozszerzenie i myślę, że niektóre inne kompilatory, które generalnie nie obsługują rozszerzeń GNU, obsługują punning typu union. Ale to nie pomaga w pisaniu czysto przenośnego kodu.
W obecnych wersjach gcc i clang, napisanie funkcji składowej C ++ przy użyciu a
switch(idx)
do wybrania elementu spowoduje optymalizację pod kątem stałych w czasie kompilacji, ale da straszny rozgałęziony asm dla indeksów czasu wykonywania. Nie ma w tym nic złegoswitch()
; jest to po prostu błąd brakującej optymalizacji w obecnych kompilatorach. Mogliby efektywnie działać kompilator switch () Slavy.Rozwiązaniem / obejściem tego problemu jest zrobienie tego w inny sposób: nadanie klasie / strukturze składowej tablicy i napisanie funkcji akcesorów, aby dołączyć nazwy do określonych elementów.
Możemy przyjrzeć się wynikom asm dla różnych przypadków użycia w eksploratorze kompilatora Godbolt . Są to kompletne funkcje Systemu V x86-64, z pominięciem końcowej instrukcji RET, aby lepiej pokazać, co otrzymasz, gdy są wbudowane. ARM / MIPS / cokolwiek byłoby podobne.
Dla porównania, odpowiedź @ Slava za pomocą a
switch()
for C ++ sprawia, że asm jest taki dla indeksu zmiennej czasu wykonywania. (Kod w poprzednim linku Godbolt).Jest to oczywiście okropne w porównaniu do wersji punningowej opartej na związkach typu C (lub GNU C ++):
źródło
[]
operatora bezpośrednio na składniku związku, Standard definiujearray[index]
jako równoważny*((array)+(index))
i ani gcc, ani clang nie rozpoznają wiarygodnie, że dostęp do*((someUnion.array)+(index))
jest dostępem dosomeUnion
. Jedynym wyjaśnieniem, jakie widzę, jest to, że StandardsomeUnion.array[index]
ani*((someUnion.array)+(index))
nie jest zdefiniowany, ale są tylko popularnymi rozszerzeniami, a gcc / clang zdecydował się nie obsługiwać drugiego, ale wydaje się, że obsługuje pierwsze, przynajmniej na razie.W C ++ jest to w większości niezdefiniowane zachowanie (zależy to od indeksu).
Z [expr.unary.op]:
&thing.a
Dlatego uważa się, że wyrażenie odnosi się do tablicy o wartości jedenint
.Od [wyr.sub]:
I z [expr.add]:
(&thing.a)[0]
jest doskonale sformułowana, ponieważ&thing.a
jest traktowana jako tablica o rozmiarze 1 i bierzemy ten pierwszy indeks. To jest dozwolony indeks.(&thing.a)[2]
narusza warunek, że0 <= i + j <= n
, ponieważ mamyi == 0
,j == 2
,n == 1
. Samo skonstruowanie wskaźnika&thing.a + 2
jest niezdefiniowanym zachowaniem.(&thing.a)[1]
jest interesującym przypadkiem. W rzeczywistości nie narusza niczego w [expr.add]. Możemy wziąć wskaźnik o jeden za koniec tablicy - co by to było. Tutaj przechodzimy do notatki w [basic.compound]:Dlatego branie wskaźnika
&thing.a + 1
jest zdefiniowanym zachowaniem, ale wyłuskiwanie go jest niezdefiniowane, ponieważ na nic nie wskazuje.źródło
(&thing.a + 1)
ciekawa sprawa, której nie opisałem. +1! ... Ciekawe, czy jesteś w komitecie ISO C ++?To jest niezdefiniowane zachowanie.
W C ++ istnieje wiele reguł, które próbują dać kompilatorowi nadzieję na zrozumienie tego, co robisz, aby mógł to uzasadnić i zoptymalizować.
Istnieją zasady dotyczące aliasingu (dostępu do danych za pomocą dwóch różnych typów wskaźników), granic tablicy itp.
Kiedy masz zmienną
x
, fakt, że nie jest ona członkiem tablicy, oznacza, że kompilator może założyć, że żaden[]
dostęp do tablicy nie może jej zmodyfikować. Więc nie musi ciągle przeładowywać danych z pamięci za każdym razem, gdy go używasz; tylko gdyby ktoś mógł go zmodyfikować z nazwy .W związku z tym
(&thing.a)[1]
można założyć, że kompilator nie odwołuje się dothing.b
. Może wykorzystać ten fakt do zmiany kolejności odczytów i zapisówthing.b
, unieważniając to, co chcesz, aby zrobić, bez unieważniania tego, co faktycznie nakazałeś.Klasycznym tego przykładem jest odrzucenie const.
tutaj zazwyczaj otrzymujesz kompilator mówiący 7, potem 2! = 7, a następnie dwa identyczne wskaźniki; pomimo faktu, że
ptr
wskazujex
. Kompilator przyjmuje fakt, żex
jest to wartość stała, aby nie zawracać sobie głowy czytaniem, gdy pytasz o wartośćx
.Ale kiedy bierzesz adres
x
, zmuszasz go do istnienia. Następnie odrzucasz const i modyfikujesz je. Tak więc rzeczywista lokalizacja w pamięci, w którejx
została zmieniona, kompilator może nie czytać jej podczas czytaniax
!Kompilator może stać się na tyle sprytny, aby wymyślić, jak nawet uniknąć śledzenia,
ptr
aby przeczytać*ptr
, ale często tak nie jest. Nieptr = ptr+argc-1
krępuj się i użyj lub trochę zamieszania, jeśli optymalizator staje się mądrzejszy od ciebie.Możesz zapewnić niestandardowy,
operator[]
który otrzyma właściwy przedmiot.posiadanie obu jest przydatne.
źródło
(&thing.a)[0]
może to zmienićx
ponieważ wie, że nie można tego zmienić w określony sposób. Podobna optymalizacja może wystąpić, gdy zmieniszb
przez,(&blah.a)[1]
jeśli kompilator może udowodnić, że nie ma zdefiniowanego dostępu,b
który mógłby to zmienić; taka zmiana może wystąpić z powodu pozornie nieszkodliwych zmian w kompilatorze, otaczającym go kodzie lub czymkolwiek. Więc nawet sprawdzenie, czy to działa, nie wystarczy.Oto sposób użycia klasy proxy w celu uzyskania dostępu do elementów w tablicy składowej według nazwy. Jest bardzo C ++ i nie ma żadnych korzyści w porównaniu z funkcjami dostępowymi zwracającymi ref, z wyjątkiem preferencji składniowych. To przeciąża
->
operatora, aby uzyskać dostęp do elementów jako elementów członkowskich, więc aby być akceptowalnym, należy zarówno nie lubić składni metod dostępu (d.a() = 5;
), jak i tolerować używanie->
z obiektem niebędącym wskaźnikiem. Spodziewam się, że może to również zmylić czytelników niezaznajomionych z kodem, więc może to być bardziej zgrabna sztuczka niż coś, co chcesz wprowadzić do produkcji.Data
Struktura w tym kodzie również przeciążenia dla operatora indeks dolny, na dostęp do jego indeksowania elementów wewnątrzar
elementu układu, jak równieżbegin
iend
funkcji, dla iteracji. Ponadto wszystkie z nich są przeładowane wersjami innymi niż const i const, które uważałem za potrzebne, aby zapewnić kompletność.Gdy
Data
„S->
służy do łączenia elementu o nazwie (tak:my_data->b = 5;
), AProxy
obiekt jest zwracany. Następnie, ponieważ taProxy
wartość r nie jest wskaźnikiem, jej własny->
operator jest wywoływany automatycznie, co zwraca wskaźnik do siebie samego. W ten sposóbProxy
obiekt jest tworzony i pozostaje ważny podczas oceny początkowego wyrażenia.Wolnostojąca z
Proxy
obiektu wypełnia jego 3 elementy odniesieniaa
,b
ic
w zależności od wskaźnika przechodzi w konstruktora, który zakłada się na miejscu w buforze zawierającym co najmniej 3 wartości których typ jest dany jako parametr szablonuT
. Więc zamiast używać nazwanych referencji, które są członkamiData
klasy, oszczędza to pamięć przez zapełnianie referencji w punkcie dostępu (ale niestety przy użyciu,->
a nie.
operatora).Aby sprawdzić, jak dobrze optymalizator kompilatora eliminuje wszystkie pośrednie zmiany wprowadzone przez użycie
Proxy
, poniższy kod zawiera 2 wersjemain()
.#if 1
Wersja używa->
i[]
operatorów, a#if 0
wersja wykonuje równoważne zestaw procedur, ale tylko przez bezpośredniego dostępuData::ar
.Nci()
Funkcji generuje wykonawcze wartość całkowitą dla inicjalizacji elementów macierzy, co zapobiega optymalizator z po podłączeniu wartości stałych bezpośrednio do każdegostd::cout
<<
połączenia.Dla gcc 6.2, używając -O3, obie wersje
main()
generują ten sam zestaw (przełączanie między#if 1
i#if 0
przed pierwszymmain()
do porównania): https://godbolt.org/g/QqRWZbźródło
main()
z funkcjami czasowymi! np.int getb(Data *d) { return (*d)->b; }
kompiluje się do justmov eax, DWORD PTR [rdi+4]
/ret
( godbolt.org/g/89d3Np ). (Tak,Data &d
uczyniłoby składnię łatwiejszą, ale użyłem wskaźnika zamiast ref, aby podkreślić dziwność przeciążania w->
ten sposób.)int tmp[] = { a, b, c}; return tmp[idx];
nie optymalizują się, więc fajnie, że tak.operator.
w C ++ 17.Jeśli odczyt wartości jest wystarczający, a wydajność nie jest problemem, lub jeśli ufasz swojemu kompilatorowi, że dobrze optymalizuje rzeczy, lub jeśli struktura ma tylko 3 bajty, możesz bezpiecznie zrobić to:
W przypadku wersji tylko C ++ prawdopodobnie chciałbyś użyć
static_assert
do sprawdzenia, czystruct data
ma standardowy układ, i być może zamiast tego zgłosić wyjątek do nieprawidłowego indeksu.źródło
Jest to nielegalne, ale istnieje obejście:
Teraz możesz zindeksować v:
źródło