Jaka jest semantyka nakładających się obiektów w C?

25

Rozważ następującą strukturę:

struct s {
  int a, b;
};

Zazwyczaj 1 , ta struktura będzie miała rozmiar 8 i wyrównanie 4.

Co jeśli utworzymy dwa struct sobiekty (a ściślej zapisujemy do przydzielonej pamięci dwa takie obiekty), przy czym drugi obiekt nakłada się na pierwszy?

char *storage = malloc(3 * sizeof(struct s));
struct s *o1 = (struct s *)storage; // offset 0
struct s *o2 = (struct s *)(storage + alignof(struct s)); // offset 4

// now, o2 points half way into o1
*o1 = (struct s){1, 2};
*o2 = (struct s){3, 4};

printf("o2.a=%d\n", o2->a);
printf("o2.b=%d\n", o2->b);
printf("o1.a=%d\n", o1->a);
printf("o1.b=%d\n", o1->b);

Czy coś w tym programie jest niezdefiniowane? Jeśli tak, to gdzie staje się niezdefiniowane? Jeśli nie jest to UB, to zawsze gwarantuje wydruk następujących informacji:

o2.a=3
o2.b=4
o1.a=1
o1.b=3

W szczególności chcę wiedzieć, co dzieje się z obiektem wskazywanym przez o1moment o2, w którym nakłada się on na siebie. Czy nadal można uzyskiwać dostęp do niezakleszczonej części ( o1->a)? Czy dostęp do części zablokowanej jest o1->bpo prostu taki sam jak dostęp o2->a?

W jaki sposób stosuje się tutaj typ skuteczny ? Reguły są wystarczająco jasne, gdy mówimy o nie nakładających się obiektach i wskaźnikach, które wskazują na to samo miejsce co ostatni sklep, ale kiedy zaczynasz mówić o efektywnym typie części obiektów lub nakładających się obiektów, jest to mniej jasne.

Czy cokolwiek by się zmieniło, gdyby drugi zapis był innego typu? Jeżeli członkowie byli powiedzieć, inta shortraczej niż dwa ints?

Oto godbolt, jeśli chcesz się z nim pobawić.


1 Ta odpowiedź dotyczy również platform, na których tak nie jest: np. Niektóre mogą mieć rozmiar 4 i wyrównanie 2. Na platformie, w której rozmiar i wyrównanie były takie same, pytanie nie miałoby zastosowania, ponieważ wyrównane, nakładające się obiekty być niemożliwe, ale nie jestem pewien, czy istnieje taka platforma.

BeeOnRope
źródło
2
Jestem prawie pewien, że to UB, ale pozwolę prawnikowi języka zapewnić rozdział i wiersz.
Barmar
Myślę, że kompilator C w starych systemach wektorowych Cray wymusił wyrównanie i rozmiar, aby były takie same, z modelem ILP64 i wymuszonym wyrównaniem 64-bitowym (adresy to 64-bitowe słowa - brak adresowania bajtów). Oczywiście spowodowało to wiele innych problemów ...
John D McCalpin

Odpowiedzi:

15

Zasadniczo jest to wszystko szara strefa w standardzie; reguła ścisłego aliasingu określa podstawowe przypadki i pozostawia czytnikowi (i dostawcom kompilatora) wypełnienie szczegółów.

Podjęto wysiłki, aby napisać lepszą regułę, ale jak dotąd nie przyniosły one żadnego tekstu normatywnego i nie jestem pewien, jaki jest jej status dla C2x.

Jak wspomniano w mojej odpowiedzi na poprzednie pytanie, najczęstszą interpretacją jest p->qto, (*p).qże skuteczny typ odnosi się do wszystkich *p, nawet jeśli później będziemy stosować .q.

Zgodnie z tą interpretacją, printf("o1.a=%d\n", o1->a);spowoduje zachowanie niezdefiniowane jako skuteczny rodzaj lokalizacji *o1nie jest s(ponieważ część z nich została nadpisana).

Uzasadnienie tej interpretacji można zobaczyć w funkcji takiej jak:

void f(s* s1, s* s2)
{
    s2->a = 5;
    s1->b = 6;
    printf("%d\n", s2->a);
}

Dzięki tej interpretacji można zoptymalizować ostatnią linię puts("5");, ale bez niej kompilator musiałby wziąć pod uwagę, że wywołanie funkcji mogło być, f(o1, o2);a zatem utracić wszystkie korzyści, które rzekomo zapewnia reguła ścisłego aliasingu.

Podobny argument dotyczy dwóch niepowiązanych typów struktur, które oba mają elementy o intróżnym przesunięciu.

MM
źródło
1
Z f(s* s1, s* s2), bez restrict, kompilator nie może zakładać s1i s2są różne wskaźniki. I pomyśleć , ponownie bez restrict, nie może nawet założyć, że nie pokrywają się częściowo. IAC, nie widzę, by f()analogia przedstawiała obawy OP . Powodzenia, niesienie. UV na pierwszą połowę.
chux - Przywróć Monikę
@ chux-ReinstateMonica bez ograniczeń, s1 == s2byłoby dozwolone, ale nie częściowe nakładanie się. (Optymalizacja w moim przykładzie kodu może być nadal wykonana, jeśli s1 == s2)
MM
@ chux-ReinstateMonica możesz również rozważyć ten sam problem z intzamiast zamiast struktur (i systemu z _Alignof(int) < sizeof(int)).
MM
3
Status tego rodzaju pytania dotyczącego typu skutecznego dla C2x jest dość otwarty i nadal podlega dyskusji w grupie analitycznej. Uważaj jednak, twierdząc, że równoważność p->qi (*p).q. Może to być prawdą dla interpretacji typu, jak twierdzisz, ale nie jest to prawdą z operacyjnego punktu widzenia. Dla równoczesnego dostępu do tej samej struktury ważne jest, aby dostęp członka nie oznaczał dostępu żadnego innego członka.
Jens Gustedt
Ścisła zasada aliasingu dotyczy dostępu . Wyrażenie po lewej stronie w E1.E2wyrażeniu nie wykonuje dostępu (mam na myśli całe E1wyrażenie. Niektóre z jego podwyrażeń mogą wykonywać dostęp. To E1znaczy (*p), jeśli jest , to odczyt wartości wskaźnika podczas oceny pjest dostępem, ale ocena *plub (*p)nie wykonuje żadnego dostęp). Ścisła reguła aliasingu nie ma zastosowania w przypadku braku dostępu.
Język Lawyer