W naszym przykładzie *(p1 + 1) = 10;
powinno to być UB, ponieważ znajduje się o jeden za końcem tablicy o rozmiarze 1. Ale jesteśmy tutaj w bardzo szczególnym przypadku, ponieważ tablica została dynamicznie skonstruowana w większej tablicy znaków.
Dynamiczne tworzenie obiektów jest opisane w 4.5 Model obiektowy C ++ [intro.object] , §3 wersji roboczej n4659 standardu C ++:
3 Jeśli tworzony jest kompletny obiekt (8.3.4) w pamięci skojarzonej z innym obiektem e typu „tablica N znaków bez znaku” lub typu „tablica N std :: bajt” (21.2.1), tablica ta zapewnia pamięć dla utworzonego obiektu, jeśli:
(3.1) - czas życia e rozpoczął się i nie skończył, oraz
(3.2) - miejsce przechowywania nowego obiektu mieści się w całości w e oraz
(3.3) - nie ma mniejszego obiektu tablicowego, który je spełnia ograniczenia.
3.3 wydaje się raczej niejasny, ale poniższe przykłady wyjaśniają cel:
struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B;
int *p = new (b->b + 4) int;
W tym przykładzie buffer
tablica zapewnia pamięć zarówno dla, jak *p1
i *p2
.
Poniższe akapity dowodzą, że kompletnym przedmiotem dla obu *p1
i *p2
jest buffer
:
4 Obiekt a jest zagnieżdżony w innym obiekcie b, jeśli:
(4.1) - a jest podobiektem z b, lub
(4.2) - b zapewnia miejsce na a lub
(4.3) - istnieje obiekt c, gdzie a jest zagnieżdżony w c , a c jest zagnieżdżone w b.
5 Dla każdego przedmiotu x istnieje jakiś przedmiot zwany kompletnym przedmiotem x, określony w następujący sposób:
(5.1) - Jeśli x jest kompletnym przedmiotem, to kompletny przedmiot x jest sam.
(5.2) - W przeciwnym razie kompletny przedmiot x jest kompletnym przedmiotem (unikalnego) obiektu, który zawiera x.
Gdy już to ustalimy, drugą istotną częścią szkicu n4659 dla C ++ 17 jest [basic.coumpound] §3 (podkreślenie moje):
3 ... Każda wartość typu wskaźnika jest jedną z następujących:
(3.1) - wskaźnik do obiektu lub funkcji (mówi się, że wskaźnik wskazuje na obiekt lub funkcję), lub
(3.2) - wskaźnik poza koniec obiektu (8.7), lub
(3.3) - wartość wskaźnika pustego (7.11) dla tego typu lub
(3.4) - niepoprawna wartość wskaźnika.
Wartość typu wskaźnika, który jest wskaźnikiem na koniec obiektu lub za nim, reprezentuje adres pierwszego bajtu w pamięci (4.4) zajmowanego przez obiekt lub pierwszego bajtu w pamięci po zakończeniu pamięci
zajmowanej przez obiekt odpowiednio. [Uwaga: wskaźnik znajdujący się poza końcem obiektu (8.7) nie jest uważany za wskazujący na niepowiązanyobiekt typu obiektu, który może znajdować się pod tym adresem. Wartość wskaźnika staje się nieważna, gdy pamięć, którą oznacza, osiągnie koniec czasu jej przechowywania; patrz 6.7. —Końcówka] Dla celów arytmetyki wskaźników (8.7) i porównań (8.9, 8.10), wskaźnik znajdujący się za końcem ostatniego elementu tablicy x n elementów jest uważany za równoważny wskaźnikowi do hipotetycznego elementu x [ n]. Reprezentacja wartości typów wskaźników jest zdefiniowana w ramach implementacji. Wskaźniki do typów zgodnych z układem muszą mieć takie same wymagania dotyczące reprezentacji wartości i wyrównania (6.11) ...
Uwaga Wskaźnik za końcem ... nie ma tutaj zastosowania, ponieważ obiekty wskazywane przez p1
i p2
nie są niepowiązane , ale są zagnieżdżone w tym samym kompletnym obiekcie, więc arytmetyka wskaźników ma sens wewnątrz obiektu, który zapewnia pamięć: p2 - p1
jest zdefiniowany i jest (&buffer[sizeof(int)] - buffer]) / sizeof(int)
czyli 1.
Więc p1 + 1
jest wskaźnikiem *p2
, *(p1 + 1) = 10;
ma zdefiniowane zachowanie i ustawia wartość *p2
.
Przeczytałem również załącznik C4 dotyczący kompatybilności między C ++ 14 a obecnymi (C ++ 17) standardami. Usunięcie możliwości stosowania arytmetyki wskaźnikowej między obiektami tworzonymi dynamicznie w pojedynczej tablicy znaków byłoby istotną zmianą, którą należałoby przytoczyć tam w IMHO, ponieważ jest to powszechnie stosowana funkcja. Ponieważ nic na ten temat nie istnieje na stronach dotyczących kompatybilności, myślę, że potwierdza to, że nie było intencją normy, aby tego zabronić.
W szczególności pokonałoby to wspólną dynamiczną konstrukcję tablicy obiektów z klasy bez domyślnego konstruktora:
class T {
...
public T(U initialization) {
...
}
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem);
for (i=0; i<N; i++) {
U u(...);
new(arr + i) T(u);
}
arr
może być następnie użyty jako wskaźnik do pierwszego elementu tablicy ...