(W nawiązaniu do tego pytania i odpowiedzi .)
Przed standardem C ++ 17 w [basic.compound] / 3 znajdowało się następujące zdanie :
Jeśli obiekt typu T znajduje się pod adresem A, wskaźnik typu cv T *, którego wartością jest adres A, wskazuje na ten obiekt, niezależnie od tego, w jaki sposób wartość została uzyskana.
Ale od czasu C ++ 17 to zdanie zostało usunięte .
Na przykład uważam, że to zdanie sprawiło, że ten przykładowy kod został zdefiniowany, a od C ++ 17 jest to niezdefiniowane zachowanie:
alignas(int) unsigned char buffer[2*sizeof(int)];
auto p1=new(buffer) int{};
auto p2=new(p1+1) int{};
*(p1+1)=10;
Przed C ++ 17 p1+1
przechowuje adres *p2
i ma właściwy typ, podobnie *(p1+1)
jak wskaźnik *p2
. W C ++ 17 p1+1
znajduje się wskaźnik poza końcem , więc nie jest wskaźnikiem do obiektu i uważam, że nie można go usunąć.
Czy taka jest interpretacja tej zmiany standardowego prawa, czy też istnieją inne zasady, które rekompensują usunięcie przytoczonego zdania?
int a, b = 0;
, nie możesz tego zrobić,*(&a + 1) = 1;
nawet jeśli zaznaczyłeś&a + 1 == &b
. Jeśli możesz uzyskać prawidłowy wskaźnik do obiektu, po prostu odgadując jego adres, to nawet przechowywanie zmiennych lokalnych w rejestrach staje się problematyczne.Odpowiedzi:
Tak, ta interpretacja jest poprawna. Wskaźnik za końcem nie jest po prostu konwertowany na inną wartość wskaźnika, która wskazuje na ten adres.
Nowy [basic.compound] / 3 mówi:
Te wzajemnie się wykluczają.
p1+1
jest wskaźnikiem za końcem, a nie wskaźnikiem do obiektu.p1+1
wskazuje na hipotetycznąx[1]
tablicę o rozmiarze 1 nap1
, a nie nap2
. Te dwa obiekty nie są konwertowalne za pomocą wskaźnika.Mamy też nienormatywną notę:
co wyjaśnia zamiar.
Jak podkreśla TC w wielu komentarzach ( szczególnie w tym ), jest to naprawdę szczególny przypadek problemu, który pojawia się przy próbie implementacji
std::vector
- czyli[v.data(), v.data() + v.size())
musi to być prawidłowy zakres, alevector
nie tworzy obiektu tablicy, więc tylko zdefiniowana arytmetyka wskaźnika będzie przechodzić od dowolnego obiektu w wektorze do końca jego hipotetycznej tablicy o jednym rozmiarze. Aby uzyskać więcej zasobów, patrz CWG 2182 , ta standardowa dyskusja i dwie wersje artykułu na ten temat: P0593R0 i P0593R1 (konkretnie sekcja 1.3).źródło
vector
problemu z wykonalnością”. +1.p1+1
nie tworzyłbyś już wskaźnika poza końcem, a cała dyskusja na temat wskaźników poza końcem jest dyskusyjna. Twój szczególny dwuelementowy przypadek specjalny może nie być UB przed 17, ale też nie jest zbyt interesujący.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.3 wydaje się raczej niejasny, ale poniższe przykłady wyjaśniają cel:
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
jestbuffer
:Gdy już to ustalimy, drugą istotną częścią szkicu n4659 dla C ++ 17 jest [basic.coumpound] §3 (podkreślenie moje):
Uwaga Wskaźnik za końcem ... nie ma tutaj zastosowania, ponieważ obiekty wskazywane przez
p1
ip2
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); // See the array as an array of N T 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 ...źródło
vector
nie tworzy (i nie może) tworzyć obiektu tablicy, ale ma interfejs, który pozwala użytkownikowi uzyskać wskaźnik obsługujący arytmetykę wskaźników (która jest zdefiniowana tylko dla wskaźników do obiektów tablicowych).Aby rozwinąć odpowiedzi udzielone tutaj, należy podać przykład tego, co moim zdaniem wyklucza zmienione sformułowanie:
Ostrzeżenie: niezdefiniowane zachowanie
#include <iostream> int main() { int A[1]{7}; int B[1]{10}; bool same{(B)==(A+1)}; std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n'; std::cout<<(same?"same":"not same")<<'\n'; std::cout<<*(A+1)<<'\n';//!!!!! return 0; }
Z powodów całkowicie zależnych od implementacji (i delikatnych) możliwe wyniki tego programu to:
0x7fff1e4f2a64 0x7fff1e4f2a60 4 same 10
To wyjście pokazuje, że dwie tablice (w tym przypadku) są przechowywane w pamięci w taki sposób, że „jedna za końcem” zawiera
A
wartość adresu pierwszego elementuB
.Zmieniona specyfikacja zapewnia, że niezależnie od
A+1
nigdy nie jest prawidłowym wskaźnikiem doB
. Stara fraza „niezależnie od sposobu uzyskania wartości” mówi, że jeśli „A + 1” wskazuje na „B [0]”, to jest to prawidłowy wskaźnik do „B [0]”. To nie może być dobre i na pewno nie jest to intencja.źródło
-O0
w niektórych kompilatorach) nie uważa wskaźników za trywialne typy. Kompilatory nie traktują poważnie wymagań standardu, podobnie jak ludzie piszący std, którzy marzą o innym języku i robią wszelkiego rodzaju wynalazki, które są bezpośrednio sprzeczne z podstawowymi zasadami. Oczywiście użytkownicy są zdezorientowani i czasami źle traktowani, gdy narzekają na błędy kompilatora.