Czy powinienem martwić się o wyrównanie podczas rzucania wskaźnikami?

W moim projekcie mamy taki fragment kodu:

// raw data consists of 4 ints
unsigned char data[16];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + 4));
i3 = *((int*)(data + 8));
i4 = *((int*)(data + 12));

Rozmawiałem z moim tech leadem, że ten kod może nie być przenośny, ponieważ próbuje rzucić unsigned char* do int*, który zwykle ma bardziej rygorystyczne wymagania. Ale tech lead mówi, że to jest w porządku, większość kompilatorów pozostaje taka sama wartość wskaźnika po castingu, a ja mogę po prostu napisać kod w ten sposób.

Szczerze mówiąc, nie jestem do końca przekonany. Po zbadaniu, znalazłem kilka osób przeciwko użyciu odlewów pointer jak wyżej, np. tutaj i tutaj. Oto moje pytania:
  1. czy to naprawdę bezpieczne, aby dereference wskaźnik po castingu w prawdziwym projekcie?
  2. czy jest jakaś różnica między castingiem w stylu C a reinterpret_cast?
  3. czy jest jakaś różnica między C A C++?
Author: Joseph Quinsey, 2012-12-14

7 answers

1. Czy to naprawdę bezpieczne, aby dereference wskaźnik po castingu w prawdziwym projekcie?

Jeśli wskaźnik nie zostanie prawidłowo wyrównany, może to naprawdę powodować problemy. Osobiście widziałem i naprawiłem błędy magistrali w prawdziwym, produkcyjnym kodzie, spowodowane odlewaniem char* do bardziej ściśle dopasowanego typu. Nawet jeśli nie pojawi się oczywisty błąd, możesz mieć mniej oczywiste problemy, takie jak wolniejsza wydajność. Ścisłe przestrzeganie standardu, aby uniknąć UB jest dobrym pomysłem, nawet jeśli nie natychmiast zobaczyć wszelkie problemy. (A jedną z reguł, które łamie kod, jest ścisła reguła aliasingu, § 3.10/10*)

Lepszą alternatywą jest użycie std::memcpy() lub std::memmove, Jeśli bufory nakładają się na siebie (lub jeszcze lepiejbit_cast<>())

unsigned char data[16];
int i1, i2, i3, i4;
std::memcpy(&i1, data     , sizeof(int));
std::memcpy(&i2, data +  4, sizeof(int));
std::memcpy(&i3, data +  8, sizeof(int));
std::memcpy(&i4, data + 12, sizeof(int));

Niektóre Kompilatory pracują ciężej niż inne, aby upewnić się, że tablice znaków są wyrównane bardziej ściśle niż jest to konieczne, ponieważ programiści tak często się mylą.

#include <cstdint>
#include <typeinfo>
#include <iostream>

template<typename T> void check_aligned(void *p) {
    std::cout << p << " is " <<
      (0==(reinterpret_cast<std::intptr_t>(p) % alignof(T))?"":"NOT ") <<
      "aligned for the type " << typeid(T).name() << '\n';
}

void foo1() {
    char a;
    char b[sizeof (int)];
    check_aligned<int>(b); // unaligned in clang
}

struct S {
    char a;
    char b[sizeof(int)];
};

void foo2() {
    S s;
    check_aligned<int>(s.b); // unaligned in clang and msvc
}

S s;

void foo3() {
    check_aligned<int>(s.b); // unaligned in clang, msvc, and gcc
}

int main() {
    foo1();
    foo2();
    foo3();
}

Http://ideone.com/FFWCjf

2. Czy jest jakaś różnica między c-style casting i reinterpret_cast?

To zależy. Odlewy w stylu C robią różne rzeczy w zależności od typów. Rzutowanie w stylu C pomiędzy typami wskaźników spowoduje to to samo, co reinterpret_cast; Zobacz § 5.4 Explicit type conversion (Cast notation) i § 5.2.9-11.

3. Czy jest jakaś różnica między C A C++?

Nie powinno być tak długo, jak masz do czynienia z typami, które są legalne w C.]}

* inny problem polega na tym, że C++ nie określa wyniku przerzucania z jednego typu wskaźnika do typu O bardziej rygorystycznych wymaganiach wyrównania. Ma to na celu wsparcie platform, na których nie można nawet reprezentować niepodpisanych wskaźników. Jednak typowe dzisiejsze platformy mogą reprezentować nieprzypisane wskaźniki, a Kompilatory określają wyniki takiego rzutu, aby były tym, czego można się spodziewać. W związku z tym problem ten jest wtórny w stosunku do naruszenia aliasingu. Zob. [expr.reinterpretacja.Obsada] / 7.

 33
Author: bames53,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-08 21:33:39

To nie jest w porządku, naprawdę. Wyrównanie może być błędne, a kod może naruszać ścisłe aliasing. Należy rozpakować go wyraźnie.

i1 = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24;

Itd. Jest to zdecydowanie dobrze zdefiniowane zachowanie, a jako bonus, jest również niezależne od endianess, w przeciwieństwie do obsady wskaźników.

 27
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-14 15:30:45

W przykładzie, który tutaj pokazujesz, to co robisz będzie bezpieczne dla prawie wszystkich nowoczesnych procesorów iff początkowy wskaźnik znaków jest poprawnie wyrównany. Ogólnie rzecz biorąc, nie jest to bezpieczne i nie gwarantuje działania.

Jeśli początkowy wskaźnik znaku nie jest prawidłowo wyrównany, będzie to działać na x86 i x86_64, ale może nie działać na innych architekturach. Jeśli masz szczęście, to po prostu da ci awarię i naprawisz swój kod. Jeśli masz pecha, niepodpisany dostęp zostanie naprawiony przez obsługę pułapki w Twoim system operacyjny i będziesz miał fatalną wydajność bez żadnych oczywistych informacji zwrotnych, dlaczego jest tak wolny (mówimy o powolnym dla jakiegoś kodu, to był ogromny problem na Alfie 20 lat temu).

Nawet na x86 & co, niepodpisany dostęp będzie wolniejszy.

Jeśli chcesz być bezpieczny dzisiaj i w przyszłości, po prostu memcpy zamiast wykonywać takie zadanie. Nowoczesny kompilator prawdopodobnie będzie miał optymalizacje dla memcpy i zrobi dobrze, a jeśli nie, memcpy sam będzie miał wykrywanie wyrównania i zrobi najszybszą rzecz.

Również Twój przykład jest błędny w jednym punkcie: sizeof (int) nie zawsze wynosi 4.

 6
Author: Art,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-14 15:53:13

Poprawnym sposobem rozpakowania char buforowanych danych jest użycie memcpy:

unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
memcpy(&i1, data, sizeof(int));
memcpy(&i2, data + sizeof(int), sizeof(int));
memcpy(&i3, data + 2 * sizeof(int), sizeof(int));
memcpy(&i4, data + 3 * sizeof(int), sizeof(int));

Rzutowanie narusza aliasing, co oznacza, że kompilator i optymalizator mogą swobodnie traktować obiekt źródłowy jako niezainicjalizowany.

Odnośnie Twoich 3 pytań:

  1. nie, dereferowanie wskaźnika rzutu jest ogólnie niebezpieczne, ze względu na aliasing i wyrównanie.
  2. Nie, w C++ casting W Stylu C jest zdefiniowany w kategoriach reinterpret_cast.
  3. Nie, C i C++ zgadzają się na aliasing oparty na cast. Jest różnica w traktowaniu aliasingu opartego na Unii (C dopuszcza to w niektórych przypadkach; C++ nie).
 4
Author: ecatmur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-14 15:57:01

Update: Przeoczyłem fakt, że rzeczywiście mniejsze typy mogą być nienaruszone w stosunku do większych, jak to może być w twoim przykładzie. Możesz usunąć ten problem, odwracając sposób, w jaki oddajesz tablicę : zadeklaruj tablicę jako tablicę int i prześlij ją do char *, Gdy chcesz uzyskać do niej dostęp w ten sposób.

// raw data consists of 4 ints
int data[4];

// here's the char * to the original data
char *cdata = (char *)data;
// now we can recast it safely to int *
i1 = *((int*)cdata);
i2 = *((int*)(cdata + sizeof(int)));
i3 = *((int*)(cdata + sizeof(int) * 2));
i4 = *((int*)(cdata + sizeof(int) * 3));

Nie będzie żadnego problemu z tablicą typów prymitywnych. Problemy z wyrównaniem występują przy radzeniu sobie z tablicami danych strukturalnych (struct w C), jeśli pierwotny typ tablicy jest większy niż typ tablicy , patrz aktualizacja powyżej.

Powinno być całkowicie ok, aby oddać tablicę znaków do tablicy int, pod warunkiem, że zastąpisz offset 4 sizeof(int), aby dopasować rozmiar int na platformie, na której ma działać kod.

// raw data consists of 4 ints
unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + sizeof(int)));
i3 = *((int*)(data + sizeof(int) * 2));
i4 = *((int*)(data + sizeof(int) * 3));

Zauważ, że otrzymasz endianness Problemy tylko wtedy, gdy udostępnisz te dane w jakiś sposób z jednej platformy na drugą z inną kolejnością bajtów. W przeciwnym razie powinno będzie dobrze.

 1
Author: didierc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-14 15:50:51

Możesz pokazać mu, jak rzeczy mogą się różnić w zależności od wersji kompilatora:

Oprócz wyrównania istnieje drugi problem: standard pozwala na rzucenie int* na char*, ale nie na odwrót (chyba że {[1] } został pierwotnie rzucony z int*). zobacz ten post po więcej szczegółów.

 1
Author: StackedCrooked,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:20

To, czy musisz się martwić o wyrównanie, zależy od wyrównania obiektu, z którego pochodzi wskaźnik.

Jeśli rzucisz na typ, który ma bardziej rygorystyczne wymagania wyrównania, nie jest przenośny.

Baza tablicy char, tak jak w twoim przykładzie, nie musi mieć dokładniejszego wyrównania niż dla typu elementu char.

Jednak wskaźnik do dowolnego typu obiektu może być konwertowany na char * i Wstecz, niezależnie od wyrównania. Wskaźnik char * zachowuje mocniejsze wyrównanie oryginału.

Możesz użyć Unii, aby utworzyć tablicę znaków, która jest bardziej wyrównana:

union u {
    long dummy; /* not used */
    char a[sizeof(long)];
};

Wszyscy członkowie Związku zaczynają pod tym samym adresem: na początku nie ma wyściółki. Gdy obiekt Unii jest zdefiniowany w storage, musi mieć wyrównanie, które jest odpowiednie dla najbardziej ściśle wyrównanego elementu.

Nasze union u powyżej jest wystarczająco wyrównane dla obiektów typu long.

Ograniczenia mogą spowodować awarię programu po przeniesieniu go na niektóre architektury. Lub może działać, ale z łagodnym lub poważnym wpływem na wydajność, w zależności od tego, czy błędnie dopasowane dostępy do pamięci są zaimplementowane w sprzęcie (kosztem niektórych dodatkowych cykli) lub w oprogramowaniu (pułapki do jądra, gdzie oprogramowanie emuluje dostęp, kosztem wielu cykli).

 0
Author: Kaz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-14 22:42:29