12

Jakie są przypadki, w których reinterpret_cast ing char* (lub char[N]) jest niezdefiniowanym zachowaniem, a kiedy jest to zdefiniowane zachowanie? Jaka jest reguła, z której powinienem skorzystać, aby odpowiedzieć na to pytanie?reinterpret_cast, char * i niezdefiniowane zachowanie


Jak dowiedzieliśmy się od this question następujące zachowanie jest niezdefiniowane:

alignas(int) char data[sizeof(int)]; 
int *myInt = new (data) int;   // OK 
*myInt = 34;       // OK 
int i = *reinterpret_cast<int*>(data); // <== UB! have to use std::launder 

Ale w którym momencie możemy zrobić reinterpret_cast na char tablicy i go NIE niezdefiniowane zachowanie? Oto kilka prostych przykładów:

  1. nr new tylko reinterpret_cast:

    alignas(int) char data[sizeof(int)]; 
    *reinterpret_cast<int*>(data) = 42; // is the first cast write UB? 
    int i = *reinterpret_cast<int*>(data); // how about a read? 
    *reinterpret_cast<int*>(data) = 4;  // how about the second write? 
    int j = *reinterpret_cast<int*>(data); // or the second read? 
    

    Kiedy robi życia na początku int? Czy jest to z deklaracją data? Jeśli tak, kiedy kończy się żywotność data?

  2. Co jeśli wskaźnik myszy byłby data?

    char* data_ptr = new char[sizeof(int)]; 
    *reinterpret_cast<int*>(data_ptr) = 4;  // is this UB? 
    int i = *reinterpret_cast<int*>(data_ptr); // how about the read? 
    
  3. Co jeśli mam tylko odbieranie konstrukcjom na drucie i chcą rzucić je warunkowo na podstawie tego, co pierwszy bajt jest?

    // bunch of handle functions that do stuff with the members of these types 
    void handle(MsgType1 const&); 
    void handle(MsgTypeF const&); 
    
    char buffer[100]; 
    ::recv(some_socket, buffer, 100) 
    
    switch (buffer[0]) { 
    case '1': 
        handle(*reinterpret_cast<MsgType1*>(buffer)); // is this UB? 
        break; 
    case 'F': 
        handle(*reinterpret_cast<MsgTypeF*>(buffer)); 
        break; 
    // ... 
    } 
    

Czy któryś z tych przypadków UB? Czy to wszystko? Czy odpowiedź na to pytanie zmienia się między C++ 11 na C++ 1z?

+0

** (1) ** wygląda na ważny dla mnie. W obu instrukcjach tworzony jest nowy obiekt 'int' i przypisywana jest jego wartość. * Czytanie * to wartość, w której wszystko zaczyna się robić włochate. To samo z ** (2) ** (przy założeniu 'sizeof (int) == 4'). ** (3) ** dla mnie wygląda jak UB. –

+0

@IgorTandetnik Rozwiązałem pytania również z pewnym odczytaniem i pozbyłem się założenia dotyczącego 'sizeof (int)', dziękuję. – Barry

+1

Teraz ** (1) ** i ** (2) ** wydają się wykazywać UB, na tych samych podstawach co połączone pytanie. Byłoby łatwo uratować, zapisując wskaźnik od pierwszego rzutu i używając go do wszystkich kolejnych zapisów i odczytów. –

Odpowiedz

3

Istnieją dwie zasady w grę tutaj:

  1. [basic.lval]/8, aka, ścisłe reguły aliasing: po prostu, nie można uzyskać dostępu do obiektu poprzez odniesienie do wskaźnika/niewłaściwy typ.

  2. [base.life]/8: po prostu, jeśli ponownie wykorzystasz pamięć do przechowywania obiektów innego typu, nie możesz użyć wskaźników do starego obiektu (ów) bez ich wcześniejszego prania.

Zasady te stanowią ważną część odróżniania "miejsca w pamięci" lub "regionu przechowywania" od "obiektu".

Wszystkie Twoje przykłady kodu paść ofiarą tego samego problemu: nie są one przedmiotem oddasz je do:

alignas(int) char data[sizeof(int)]; 

który tworzy obiekt typu char[sizeof(int)]. Obiekt ten to , a nie i int. Dlatego nie możesz uzyskać do niego dostępu tak, jakby był. Nie ma znaczenia, czy jest to czytanie czy pisanie; nadal prowokujesz UB.

Podobnie:

char* data_ptr = new char[sizeof(int)]; 

To tworzy również obiekt typu char[sizeof(int)].

char buffer[100]; 

Stwarza to obiekt typu char[100]. Ten obiekt nie jest ani MsgType1, ani MsgTypeF. Więc nie możesz uzyskać do niego dostępu tak, jakby to było.

Należy zauważyć, że UB jest tutaj, gdy uzyskujesz dostęp do bufora jako jeden z typów Msg*, a nie kiedy sprawdzasz pierwszy bajt. Jeśli wszystkie typy Msg* są trywialnie kopiowalne, to jest całkowicie dopuszczalne, aby odczytać pierwszy bajt, a następnie skopiować bufor do obiektu odpowiedniego typu.

switch (buffer[0]) { 
case '1': 
    { 
     MsgType1 msg; 
     memcpy(&msg, buffer, sizeof(MsgType1); 
     handle(msg); 
    } 
    break; 
case 'F': 
    { 
     MsgTypeF msg; 
     memcpy(&msg, buffer, sizeof(MsgTypeF); 
     handle(msg); 
    } 
    break; 
// ... 
} 

Pamiętaj, że mówimy o tym, jakie stany językowe będą niezdefiniowane. Szanse są dobre, że kompilator będzie w porządku z każdym z nich.

Czy odpowiedź na to pytanie zmienia się między C++ 11 na C++ 1z?

Istnieją pewne znaczące zasada wyjaśnienia ponieważ C++ 11 (szczególnie [basic.life]). Ale zamiary stojące za regułami się nie zmieniły.

+0

Nie deklaruję, że moja tablica 'char' nie może potencjalnie uzyskać pamięci dla jakiegoś, jeszcze nie zainicjowanego, typu" T "? W tym sensie, czy zdrowe posypanie 'praniem' nie uczyni wszystkiego dobrze zdefiniowanego? – Barry

+0

@Barry: [Nie o to chodzi w przypadku 'std :: launder' (http://stackoverflow.com/a/39382728/734069). Jeśli rozpoczniesz życie obiektu w pamięci starszej, możesz uzyskać wskaźnik do nowego obiektu od wskaźnika do starego. Nie rozpoczyna się życia niczego. "* Nie deklaruję, że moja tablica znaków nie stanowi potencjalnie uzyskania pamięci dla jakiegoś przyszłego, automatycznie inicjowanego typu T? *" Według tej logiki * każdy obiekt * może być "jeszcze nierozpoznanym inicjowanym typem" T ". W końcu obiekt ma pamięć. 'char [X]' jest tak samo obiektem jak każdy inny obiekt. –

+0

Ale to jest, gdy [basic.life] mówi, że czas życia obiektu zaczyna się - kiedy zostanie uzyskane miejsce. Podano 'char buf [4]; int * i = new (buf) int; ', kiedy rozpoczyna się żywotność' int' wskazywanej przez 'i'? – Barry