В моем проекте у нас есть такой код:
// 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));
Я говорил с моим техническим руководителем, что этот код не может быть переносимым, так как он пытается unsigned char*
к int*
который обычно имеет более строгое требование выравнивания. Но технический лидер говорит, что все в порядке, большинство компиляторов остается неизменным значением указателя после приведения, и я могу просто написать такой код.
Честно говоря, я не совсем уверен. После исследования я обнаружил, что некоторые люди против использования приведений указателей, как указано выше, например, Вот а также Вот.
Итак, вот мои вопросы:
reinterpret_cast
?1. ДЕЙСТВИТЕЛЬНО ли безопасно разыменовать указатель после приведения в реальном проекте?
Если указатель не выровнен должным образом, это может вызвать проблемы. Я лично видел и исправлял ошибки шины в реальном, производственном коде, вызванном приведением char*
к более строго выровненному типу. Даже если вы не получите очевидную ошибку, у вас могут возникнуть менее очевидные проблемы, такие как снижение производительности. Строгое следование стандарту во избежание UB — хорошая идея, даже если вы сразу не видите никаких проблем. (И одно из правил, которое нарушает код, — это строгое правило псевдонимов, § 3.10 / 10 *)
Лучшая альтернатива — использовать std::memcpy()
или же std::memmove
если буферы перекрываются (или еще лучше bit_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));
Некоторые компиляторы работают больше, чем другие, чтобы убедиться, что массивы символов выровнены более строго, чем необходимо, потому что программисты часто ошибаются в этом.
#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();
}
2. Есть ли разница между приведением в стиле C и reinterpret_cast?
Это зависит. Приведения в стиле C делают разные вещи в зависимости от используемых типов. Приведение в стиле C между указателями приводит к тому же, что и reinterpret_cast; См. П. 5.4 Явное преобразование типов (нотация приведения) и § 5.2.9-11.
3. Есть ли разница между C и C ++?
Не должно быть так долго, пока вы имеете дело с типами, которые допустимы в C.
* Другая проблема заключается в том, что C ++ не определяет результат приведения из одного типа указателя к типу с более строгими требованиями к выравниванию. Это делается для поддержки платформ, на которых указатели без выравнивания даже не могут быть представлены. Однако сегодня типичные платформы могут представлять собой невыровненные указатели, а компиляторы указывают, что результаты такого приведения будут такими, как вы ожидаете. Таким образом, эта проблема является вторичной по отношению к нарушению псевдонимов. См. [Expr.reinterpret.cast] / 7.
Это не хорошо, правда. Выравнивание может быть неправильным, и код может нарушать строгий псевдоним. Вы должны распаковать это явно.
i1 = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24;
и т.д. Это определенно четко определенное поведение, и в качестве бонуса оно также не зависит от порядка байтов, в отличие от вашего указателя.
В примере, который вы здесь показываете, то, что вы делаете, будет безопасно почти на всех современных процессорах, если исходный указатель на символ правильно выровнен. В целом, это небезопасно и не гарантирует работу.
Если начальный указатель на символ не выровнен правильно, это будет работать на x86 и x86_64, но может не работать на других архитектурах. Если вам повезет, это просто даст вам сбой, и вы исправите свой код. Если вам не повезло, доступ к невыровненному будет исправлен обработчиком ловушек в вашей операционной системе, и вы будете иметь ужасную производительность без какой-либо очевидной обратной связи о том, почему она такая медленная (для некоторого кода мы говорим крайне медленно, это было огромной проблемой на альфе 20 лет назад).
Даже на х86 & co, не выровненный доступ будет медленнее.
Если вы хотите быть в безопасности сегодня и в будущем, просто memcpy
вместо того, чтобы делать назначение, как это. Современный компилятор, вероятно, будет иметь оптимизации для memcpy
и делать правильные вещи, а если нет, memcpy
Сам будет иметь обнаружение выравнивания и сделает самую быструю вещь.
Кроме того, ваш пример неверен в одном: sizeof (int) не всегда 4.
Правильный способ распаковать char
буферизованные данные должны использовать 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));
Преобразование нарушает псевдонимы, что означает, что компилятор и оптимизатор могут обрабатывать исходный объект как неинициализированный.
По поводу ваших 3 вопросов:
reinterpret_cast
,Обновить:
Я упустил из виду тот факт, что действительно меньшие типы могут быть выровнены относительно большего, как это может быть в вашем примере. Вы можете решить эту проблему, изменив способ преобразования массива: объявите массив как массив типа int и приведите его к char *
когда вам нужно получить к нему доступ таким образом.
// 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));
Не будет никаких проблем с массивом типов примитивов. Проблемы выравнивания возникают при работе с массивами структурированные данные (struct
в с), если исходный тип примитива массива больше, чем тот, к которому он приведен, см. обновление выше.
Должно быть совершенно нормально привести массив char к массиву int, если вы замените смещение 4 на sizeof(int)
, чтобы соответствовать размеру int на платформе, на которой должен выполняться код.
// 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));
Обратите внимание, что вы получите порядок байт проблемы только в том случае, если вы как-то делитесь этими данными с одной платформы на другую с другим порядком байтов. В противном случае все должно быть прекрасно.
Вы можете показать ему, как все может отличаться в зависимости от версии компилятора:
Помимо выравнивания есть еще одна проблема: стандарт позволяет вам int*
в char*
но не наоборот (если только char*
был изначально отлит из int*
). Смотрите этот пост для более подробной информации.
Нужно ли вам беспокоиться о выравнивании, зависит от выравнивания объекта, от которого произошел указатель.
Если вы приведете к типу с более строгими требованиями к выравниванию, он не будет переносимым.
Основание char
массив, как в вашем примере, не обязательно должен иметь более строгое выравнивание, чем для типа элемента char
,
Тем не менее, указатель на любой тип объекта может быть преобразован в char *
и обратно, независимо от выравнивания. char *
Указатель сохраняет более сильное выравнивание оригинала.
Вы можете использовать объединение для создания массива char, который выровнен более строго:
union u {
long dummy; /* not used */
char a[sizeof(long)];
};
Все члены профсоюза начинаются с одного и того же адреса: в начале нет отступов. Когда объект объединения определен в хранилище, он должен иметь выравнивание, которое подходит для наиболее строго выровненного элемента.
наш union u
Выше выровнен достаточно строго для объектов типа long
,
Нарушение ограничений выравнивания может привести к сбою программы при переносе на некоторые архитектуры. Или это может сработать, но с незначительным или серьезным влиянием на производительность, в зависимости от того, реализован ли неправильно выровненный доступ к памяти на аппаратном уровне (за счет некоторых дополнительных циклов) или в программном обеспечении (ловушки для ядра, где программное обеспечение эмулирует доступ, за плату). много циклов).