2017-09-02 32 views
7

Jak wyjaśniono w P0532R0 w folowing przypadku użycia std::launder muszą być stosowane w celu uniknięcia niezdefiniowane zachowanie (UB):Czy "pranie" jest propagowane przez arytmetykę wskaźnika?

struct X{ 
    const int i; 
    x(int i):i{i}{} 
    }; 

unsigned char buff[64]; 
auto p = new(buff) X(33); 
p->~X(); 
new(buff) X(42); 
p = std::launder(p); 
assert(p->i==42); 

Ale to, co stało się w przypadku, gdy więcej niż jeden obiekt jest na buforze (to jest dokładnie co by się stało, gdyby jeden popycha 2 X w wektorze, czyści wektor, a następnie wypycha dwóch nowych X):

unsigned char buff[64]; 
auto p0 = new(buff) X(33); 
auto p1 = new(p0+1) X(34); 
p1->~X(); 
p0->~X(); 
new(buff) X(42); 
new(p0+1) X(43); 
p0 = std::launder(p0); 
assert(p0->i==42); 
assert(p0[1].i==43);//??? 

Czy ostatnie stwierdzenie poprawne, czy p0[1] nadal wywołuje UB?

+2

Czy nie jest "assert (p0 [1] == 43);" nieprawidłowe wyrażenie? ... Biorąc pod uwagę typ klasy tworzony przez podwyrażenie 'p0 [1]' nie ma przeciążonego 'operatora == (X, int)' – WhiZTiM

+0

@ WhiZTiM Oczywiste literówka jest oczywista? – Barry

+0

To był literówka. – Oliv

Odpowiedz

5

Twój kod wywołuje UB, ale nie dla przyczyn launder. To dlatego, że p0[1].i sam jest UB.

Tak naprawdę ([Expr.Add]/4)

Gdy wyrażenie ma integralną typu jest dodawane lub odejmowane od wskaźnika wynik musi typ argumentu wskaźnika. Jeśli wyrażenie P wskazuje na element x [i] obiektu tablicy x z n elementami, wyrażenia P + J i J + P (gdzie J ma wartość j) wskazują na (prawdopodobnie hipotetyczny) element x [i + j] jeśli 0 ≤ i + j ≤ n; w przeciwnym razie zachowanie jest niezdefiniowane. Podobnie wyrażenie P - J wskazuje na (prawdopodobnie hipotetyczny) element x [i - j], jeśli 0 ≤ i - j ≤ n; w przeciwnym razie zachowanie jest niezdefiniowane.

Obiekt, który nie jest elementem tablicy, jest uważany za należący do tablicy jednoelementowej do tego celu; patrz 8.3.1. Wskaźnik za ostatnim elementem tablicy x n elementów uważa się za równoważny wskaźnikowi do hipotetycznego elementu x [n] w tym celu; patrz 6.9.2.

[] po zastosowaniu do wskaźnika oznacza wykonanie arytmetycznej wskazówki. W modelu obiektowym C++ arytmetykę wskaźnika można używać tylko na wskaźnikach do elementów w tablicy wskazanego typu. Zawsze możesz traktować obiekt jako tablicę o długości 1, dzięki czemu możesz uzyskać wskaźnik "jeden za końcem" pojedynczego obiektu. Tak więc, p0 + 1 jest ważny.

Co to jest nie valid uzyskuje dostęp do obiektu przechowywanego pod tym adresem, mimo że wskaźnik uzyskano za pomocą p0 + 1. Oznacza to, że p0[1].i jest niezdefiniowanym zachowaniem. To jest tak jak UB przed pranie jako po.

Teraz spójrzmy na inną możliwość:

X x[2]; 
x[1].~X(); //Destroy this object. 
new(x + 1) X; //Construct a new one. 

Warto więc zadać kilka pytań:

Czy x[1] UB? Powiedziałbym ... nie, to nie jest UB. Czemu? Ponieważ x[1] nie jest:

wskaźnik, który zwrócił się do oryginalnego obiektu, odniesienie, o którym mowa oryginalnego obiektu lub nazwa oryginalnego obiektu

x punkty do tablicy i pierwszy element tej tablicy, a nie drugi element. Dlatego nie wskazuje na oryginalny obiekt. Nie jest to odniesienie, ani też nazwa tego obiektu.

Dlatego nie kwalifikuje się do ograniczeń określonych przez [basic.life]/8. Tak więc x[1] powinien wskazywać na nowo skonstruowany obiekt.

Biorąc pod uwagę, że w ogóle nie potrzebujesz launder.

Jeśli robisz to w sposób legalny, nie potrzebujesz tutaj launder.

+0

Zaczekaj chwilę. Czy to oznacza, że ​​robienie wskaźnika arytmetycznego na wskaźniku zwróconym przez 'std :: vector :: data()' to także UB? Ponieważ jest to w zasadzie przykład 1: zbiór obiektów utworzonych w wewnętrznym niezainicjowanym buforze. –

+2

@Revolver_Ocelot Dokładnie. Zobacz [CWG2182] (https://wg21.link/CWG2182). – Barry

+1

@Revolver_Ocelot: Nie, wykonanie arytmetycznej wskaźnika na tym, co zwraca 'wektor' jest w porządku. Ale to dlatego, że standard * wyraźnie określa *, że jest w porządku. 'vector' jest częścią standardowej biblioteki i dlatego może wykonywać rzeczy zdefiniowane w ramach implementacji, które byłyby UB dla użytkowników. W skrócie, zasada ta oznacza, że ​​nie możesz legalnie zaimplementować 'vector'. –

2

Powodem std::launder jest potrzebne w pierwszej kolejności ze względu na tego naruszenia z [basic.life]

Jeżeli po żywotność obiektu została zakończona i przed przechowywania którym obiekt zajęte są ponownie wykorzystywane lub uwolniony, A nowy obiekt jest tworzony w miejscu przechowywania, w którym pierwotny obiekt zajmował, wskaźnik wskazujący na oryginalny obiekt, odniesienie odwołujące się do oryginalnego obiektu, lub nazwa oryginalnego obiektu będzie automatycznie odnosić się do nowego obiektu i, po trwałość nowego obiektu została rozpoczęta, można go wykorzystać do manipulowania nowym obiektem, jeśli: [...]

  • Typ pierwotnego obiektu nie jest const-qualified, a jeśli jest typem klasy, nie zawiera żadnych statycznych elementów danych, których typ jest const-kwalifikowany lub typu referencyjnego, i [...]

czyli bez std::launder, p przy wskaźnik nie wskazują na nowo skonstruowaną obiektu.

Jeżeli te warunki nie są spełnione, to wskaźnik do nowego obiektu można uzyskać od wskaźnika, który reprezentuje adres jego przechowywania wywołując std​::​launder

dlatego std::launder robi what it does.

Od [ptr.launder], trafnie zatytułowany Pointer optymalizacja bariera

Jeśli nowy obiekt jest tworzony w pamięci zajmowanej przez istniejącego obiektu tego samego typu, wskaźnik do oryginalnego obiektu może być stosowany w odniesieniu do nowy obiekt, chyba że typ zawiera elementy stałe lub odniesienia; w tych ostatnich przypadkach funkcja ta może być użyta do uzyskania użytecznego wskaźnika do nowego obiektu.

Co oznacza, że ​​oryginalny wskaźnik nie może być używany w odniesieniu do nowo zbudowanego obiektu, chyba że został wyprany.

Z tego, co widzę, można interpretować oba sposoby (może być całkowicie błędne).

  • Wskaźnik obliczane z wypranego wskaźnik nie jest oryginalna wskaźnik, więc jest dobrze uformowane
  • Nigdzie nie wspomniano, że wskaźnik oblicza się z wypranego wskaźnik jest prania, więc jego UB

Osobiście uważam, że pierwszy jest prawdziwy, ponieważ std::launder jest barierą optymalizacji wskaźnika.