W najściślejszym sensie nie jest to zachowanie niezdefiniowane, ale zdefiniowane w implementacji. Tak więc, choć niewskazane jest, jeśli planujesz wspierać architekturę spoza głównego nurtu, prawdopodobnie możesz to zrobić.
Standardowy cytat podany przez interjay jest dobry, wskazując UB, ale moim zdaniem jest to drugi najlepszy hit, ponieważ dotyczy arytmetyki wskaźnik-wskaźnik (zabawnie, jeden jest jawnie UB, a drugi nie). W pytaniu znajduje się akapit dotyczący operacji bezpośrednio:
[expr.post.incr] / [expr.pre.incr] Operandem
będzie [...] lub wskaźnik do całkowicie zdefiniowanego typu obiektu.
Och, poczekaj chwilę, całkowicie zdefiniowany typ obiektu? To wszystko? To znaczy, tak naprawdę, wpisać ? Więc w ogóle nie potrzebujesz przedmiotu?
Potrzebne jest sporo czytania, aby znaleźć wskazówkę, że coś w tym miejscu może nie być tak dobrze zdefiniowane. Ponieważ do tej pory brzmi to tak, jakbyś mógł to zrobić bez żadnych ograniczeń.
[basic.compound] 3
wypowiada się o tym, jaki typ wskaźnika można mieć, a ponieważ nie ma żadnej z pozostałych trzech, wynik twojej operacji wyraźnie mieści się w 3.4: nieprawidłowy wskaźnik .
Nie oznacza to jednak, że nie możesz mieć nieprawidłowego wskaźnika. Przeciwnie, wymienia niektóre bardzo powszechne, normalne warunki (np. Koniec okresu przechowywania), w których wskaźniki regularnie stają się nieważne. Tak więc najwyraźniej jest to dozwolone. I rzeczywiście:
[basic.stc] 4 Przekierowanie
przez niepoprawną wartość wskaźnika i przekazanie niepoprawnej wartości wskaźnika do funkcji zwolnienia ma niezdefiniowane zachowanie. Każde inne użycie nieprawidłowej wartości wskaźnika ma działanie zdefiniowane w implementacji.
Robimy tam „każdy inny”, więc nie jest to zachowanie nieokreślone, ale zdefiniowane w ramach implementacji, dlatego ogólnie dopuszczalne (chyba że implementacja wyraźnie mówi coś innego).
Niestety, to nie koniec historii. Chociaż wynik netto nie zmienia się odtąd, staje się coraz bardziej mylący, im dłużej będziesz szukać „wskaźnika”:
[basic.compound]
Prawidłowa wartość typu wskaźnika obiektu reprezentuje adres bajtu w pamięci lub wskaźnik zerowy. Jeśli obiekt typu T znajduje się pod adresem A, mówi się, że [...] wskazuje na ten obiekt, niezależnie od tego, w jaki sposób uzyskano wartość .
[Uwaga: Na przykład adres znajdujący się za końcem tablicy będzie uważany za wskazujący na niepowiązany obiekt typu elementu tablicy, który może znajdować się pod tym adresem. [...]].
Czytaj jako: OK, kogo to obchodzi! Tak długo, jak wskazuje wskaźnik gdzieś w pamięci , jestem dobry?
[basic.stc.dynamic.safety] Wartość wskaźnika to bezpiecznie uzyskany wskaźnik [bla bla]
Odczytaj jako: OK, bezpiecznie uzyskany, cokolwiek. Nie wyjaśnia, co to jest, ani nie mówi, że faktycznie go potrzebuję. Bezpiecznie wyprowadzony z cholery. Najwyraźniej nadal mogę mieć niezabezpieczone wskaźniki. Domyślam się, że dereferencjowanie ich prawdopodobnie nie byłoby tak dobrym pomysłem, ale ich posiadanie jest całkowicie dozwolone. Nie mówi inaczej.
Implementacja może mieć zmniejszone bezpieczeństwo wskaźnika, w którym to przypadku ważność wartości wskaźnika nie zależy od tego, czy jest to bezpiecznie uzyskana wartość wskaźnika.
Och, więc to może nie mieć znaczenia, tak jak myślałem. Ale czekaj ... „może nie”? Oznacza to, że może również . Skąd mam wiedzieć?
Alternatywnie implementacja może mieć ścisłe bezpieczeństwo wskaźnika, w którym to przypadku wartość wskaźnika, która nie jest bezpiecznie uzyskaną wartością wskaźnika, jest nieprawidłową wartością wskaźnika, chyba że wskazany pełny obiekt ma dynamiczny czas przechowywania i wcześniej został uznany za osiągalny
Czekaj, więc to możliwe, że muszę wywoływać declare_reachable()
każdy wskaźnik? Skąd mam wiedzieć?
Teraz możesz przekonwertować na intptr_t
, który jest dobrze zdefiniowany, dając całkowitą reprezentację bezpiecznie uzyskanego wskaźnika. Dla których oczywiście, jako liczba całkowita, jest całkowicie uzasadnione i dobrze zdefiniowane, aby zwiększać ją według własnego uznania.
I tak, możesz przekonwertować intptr_t
powrót na wskaźnik, który jest również dobrze zdefiniowany. Po prostu, nie będąc oryginalną wartością, nie ma już gwarancji, że masz bezpiecznie wyprowadzony wskaźnik (oczywiście). Mimo wszystko, zgodnie z literą standardu, podczas gdy jest definiowany jako implementacja, jest to w 100% uzasadniona czynność:
[expr.reinterpret.cast] 5
Wartość typu całkowego lub typu wyliczenia można jawnie przekonwertować na wskaźnik. Wskaźnik skonwertowany na liczbę całkowitą o wystarczającym [...] rozmiarze i z powrotem na tę samą pierwotną wartość wskaźnika [...]; odwzorowania między wskaźnikami i liczbami całkowitymi są w inny sposób zdefiniowane w implementacji.
Haczyk
Wskaźniki są zwykłymi liczbami całkowitymi, tylko ty używasz ich jako wskaźników. Och, gdyby to tylko prawda!
Niestety, istnieją architektury, w których to wcale nie jest prawdą, a samo wygenerowanie niepoprawnego wskaźnika (nie dereferencjowanie go, po prostu umieszczenie go w rejestrze wskaźnika) spowoduje pułapkę.
To jest podstawa „zdefiniowanej implementacji”. To oraz fakt, że zwiększanie wskaźnika w dowolnym momencie, jak możesz, może oczywiście spowodować przepełnienie, z którym standard nie chce sobie poradzić. Koniec przestrzeni adresowej aplikacji może nie pokrywać się z lokalizacją przepełnienia, a nawet nie wiadomo, czy istnieje coś takiego jak przepełnienie wskaźników dla określonej architektury. Podsumowując, jest to koszmarny bałagan, bez związku z możliwymi korzyściami.
Z drugiej strony radzenie sobie z warunkiem jednego obiektu przeszłości jest łatwe: implementacja musi po prostu upewnić się, że żaden obiekt nie jest nigdy przydzielony, aby zajęty został ostatni bajt w przestrzeni adresowej. Jest to dobrze zdefiniowane, ponieważ jest użyteczne i trywialne do zagwarantowania.