Должен ли я беспокоиться о выравнивании во время каста? - программирование
Подтвердить что ты не робот

Должен ли я беспокоиться о выравнивании во время каста?

В моем проекте у нас есть такой код:

// 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*, который обычно имеет более строгие требования к выравниванию. Но технический руководитель говорит, что все в порядке, большинство компиляторов остается тем же самым значением указателя после кастинга, и я могу просто написать такой код.

Чтобы быть откровенным, я не уверен. После исследования я нахожу некоторых людей против использования отливок, таких как выше, например здесь и здесь.

Итак, вот мои вопросы:

  • Действительно ли безопасно разыменовывать указатель после кастинга в реальном проекте?
  • Есть ли разница между C-style casting и reinterpret_cast?
  • Есть ли разница между C и С++?
4b9b3361

Ответ 1

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));

Некоторые компиляторы работают сложнее, чем другие, чтобы убедиться, что массивы char выровнены более строго, чем необходимо, потому что программисты так часто ошибаются.

#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. Есть ли разница между C-style casting и reinterpret_cast?

Это зависит. C-style cast делает разные вещи в зависимости от используемых типов. C-style casting между типами указателей приведет к тому же, что и reinterpret_cast; См. § 5.4. Явное преобразование типов (литая нотация) и § 5.2.9-11.

3. Есть ли разница между C и С++?

Не должно быть до тех пор, пока вы имеете дело с типами, которые являются законными в C.


* Другая проблема заключается в том, что С++ не указывает результат литья из одного типа указателя в тип с более строгими требованиями к выравниванию. Это для поддержки платформ, где не выровненные указатели даже не могут быть представлены. Однако типичные платформы сегодня могут представлять собой невыровненные указатели, а компиляторы определяют результаты такого приведения как то, что вы ожидаете. Таким образом, этот вопрос является вторичным по отношению к нарушению псевдонимов. См. [Expr.reinterpret.cast]/7.

Ответ 2

Это нехорошо, правда. Выравнивание может быть неправильным, и код может нарушать строгий псевдоним. Вы должны распаковать его явно.

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

и т.д.. Это определенно четко определенное поведение, и в качестве бонуса он также независим от контентоспособности, в отличие от вашего указателя.

Ответ 3

В примере, который вы показываете, то, что вы делаете, будет безопасным почти для всех современных процессоров, если исходный указатель char правильно выровнен. В целом это небезопасно и не гарантируется работа.

Если исходный указатель char неправильно выровнен, это будет работать на x86 и x86_64, но может быть неудачным на других архитектурах. Если вам повезет, это просто вызовет крах, и вы исправите свой код. Если вам не повезло, неравномерный доступ будет исправлен обработчиком ловушки в вашей операционной системе, и у вас будет ужасная производительность без каких-либо очевидных отзывов о том, почему это так медленно (мы говорим о медленном медленном для некоторого кода, это была огромная проблема для альфы 20 лет назад).

Даже на x86 и co, неравномерный доступ будет медленнее.

Если вы хотите быть в безопасности сегодня и в будущем, просто memcpy вместо выполнения этого задания. Современный корреспондент, скорее всего, будет оптимизировать для memcpy и делать правильные вещи, а если нет, memcpy сам будет иметь определение выравнивания и сделает самую быструю вещь.

Кроме того, ваш пример неверен в одной точке: sizeof (int) не всегда 4.

Ответ 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));

Кастинг нарушает сглаживание, что означает, что компилятор и оптимизатор могут свободно рассматривать исходный объект как неинициализированный.

Относительно трех вопросов:

  • Нет, разыменование указателя на литье в целом небезопасно из-за сглаживания и выравнивания.
  • Нет, в С++ кастинг C-стиля определяется с помощью reinterpret_cast.
  • Нет, C и С++ согласны с наложением на основе литья. Существует разница в обработке псевдонимов на основе объединения (C разрешает его в некоторых случаях, С++ - нет).

Ответ 5

Обновление: Я упускал из виду тот факт, что действительно меньшие типы могут быть неравномерными относительно более крупного, как это может быть в вашем примере. Вы можете облегчить эту проблему, изменив способ, которым вы создаете массив: объявите массив как массив int и передайте его char *, когда вам нужно получить к нему доступ таким образом.

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

// here 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 в C), если исходный тип примитива массива больше, чем тот, на который он отправлен, см. обновление выше.

Должно быть отлично, чтобы передать массив из 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));

Обратите внимание, что вы получите endianness только в том случае, если вы каким-либо образом передадите эти данные с одной платформы на другую с другим порядком байтов. В противном случае это должно быть прекрасно.

Ответ 6

Возможно, вам захочется показать ему, как все может различаться в зависимости от версии компилятора:

Помимо выравнивания есть вторая проблема: стандарт позволяет вам отбрасывать int* в char*, но не наоборот (если только char* не был изначально выбран из int*). Подробнее см. в этом сообщении.

Ответ 7

Следует ли беспокоиться о выравнивании, зависит от выравнивания объекта, из которого возник этот указатель.

Если вы применяете тип, который имеет более строгие требования к выравниванию, он не переносится.

База массива char, как и в вашем примере, не требует более строгого выравнивания, чем для типа элемента char.

Однако указатель на любой тип объекта может быть преобразован в char * и обратно, независимо от выравнивания. Указатель char * сохраняет более сильное выравнивание оригинала.

Вы можете использовать объединение для создания массива char, который более сильно выровнен:

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

Все члены профсоюза начинаются по одному адресу: в начале нет прокладки. Когда объект объединения определен в хранилище, он должен поэтому иметь выравнивание, которое подходит для наиболее строго выровненного элемента.

Наше union u выше выровнено достаточно строго для объектов типа long.

Нарушение ограничений выравнивания может привести к сбою программы при ее переносе на некоторые архитектуры. Или это может сработать, но с умеренным и сильным воздействием на производительность, в зависимости от того, реализованы ли неправильные обращения к памяти в аппаратном обеспечении (за счет некоторых дополнительных циклов) или в программном обеспечении (ловушки для ядра, где программное обеспечение эмулирует доступ, по цене из многих циклов).