С++ Строгое правило псевдонимов. Является ли освобождение псевдонимов "char" улицей с 2 ​​направлениями?

Всего пару недель назад я узнал, что стандарт С++ имеет строгое правило псевдонимов. В принципе, я задал вопрос о смещении битов - вместо того, чтобы менять каждый байт по одному, чтобы максимизировать производительность, я хотел загрузить собственный собственный процессорный регистр (соответственно 32 или 64 бита) и выполнить сдвиг 4/8 байтов в одной команде.

Это код, который я хотел бы избежать:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };

for (int i = 0; i < 3; ++i)
{
  buffer[i] <<= 4; 
  buffer[i] |= (buffer[i + 1] >> 4);
}
buffer[3] <<= 4;

И вместо этого я хотел использовать что-то вроде:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };
unsigned int *p = (unsigned int*)buffer; // unsigned int is 32 bit on my platform
*p <<= 4;

Кто-то вызвал в комментарии, что мое предложенное решение нарушило правила слияния С++ (поскольку p был типа int*, а буфер был типа char*, и я разыменовал p для выполнения сдвига. (Пожалуйста, игнорируйте возможные проблемы выравнивания и порядка байтов - я обрабатываю те, что находятся за пределами этого фрагмента). Я был очень удивлен, узнав о правиле строкового слияния, поскольку я регулярно работаю с данными из буферов, отбрасывая их от одного типа к другому и никогда не имел никаких проблем. расследование показало, что используемый мною компилятор (MSVC) не применяет строгие правила псевдонимов, и поскольку я только разрабатываю gcc/g++ в свое свободное время в качестве хобби, я, вероятно, еще не сталкивался с проблемой.

Итак, я задал вопрос о строгих правилах псевдонимов и новом операторе размещения на С++:

IsoCpp.org предлагает часто задаваемые вопросы о размещении новых и предоставляет следующий пример кода:

#include <new>        // Must #include this to use "placement new"
#include "Fred.h"     // Declaration of class Fred
void someCode()
{
  char memory[sizeof(Fred)];     // Line #1
  void* place = memory;          // Line #2
  Fred* f = new(place) Fred();   // Line #3 (see "DANGER" below)
  // The pointers f and place will be equal
  // ...
}

Пример достаточно прост, но я спрашиваю себя: "Что, если кто-то вызовет метод на f - например, f->talk()? В этот момент мы будем разыменовывать f, что указывает на то же в памяти memory (типа char*. Я читал множество мест, где есть исключения для переменных типа char* для псевдонимов любого типа, но у меня создалось впечатление, что это не" два -worth street "- означает, char* может псевдоним (чтение/запись) любого типа T, но тип T может использоваться только для псевдонима char*, если T имеет значение char*. Когда я набираю это, это не имеет никакого смысла для меня, и поэтому я склоняюсь к убеждению, что утверждение о том, что мой первоначальный (пример смещения битов) нарушил правило строгой псевдонимы, неверен.

Может кто-нибудь объяснить, что правильно? Я сходил с ума, пытаясь понять, что является законным, а что нет (несмотря на то, что читал многочисленные сайты и сообщения SO по теме)

Спасибо

Ответ 1

Правило aliasing означает, что только язык promises для ваших указаний на указатель должен быть действительным (т.е. не запускать поведение undefined), если:

  • Вы получаете доступ к объекту через указатель совместимого класса: либо его фактический класс, либо один из его суперклассов, правильно отбрасываемый. Это означает, что если B является суперклассом D и у вас есть D* d, указывающий на действительный D, доступ к указателю, возвращаемому static_cast<B*>(d), в порядке, но доступ к которому возвращен reinterpret_cast<B*>(d) не является. Последнее, возможно, не учитывало компоновку под-объекта B внутри D.
  • Вы получаете доступ к нему с помощью указателя на char. Поскольку char имеет размер байт и выровнен по байтам, вы не сможете читать данные из char*, будучи в состоянии читать его с D*.

Тем не менее, другие правила в стандарте (в частности, те, что относятся к макету макета и типам POD), можно читать как гарантирующие, что вы можете использовать указатели и reinterpret_cast<T*> для псевдонимов между двумя типами POD и массивами char, если вы должны иметь массив char подходящего размера и выравнивания.

Другими словами, это законно:

int* ia = new int[3];
char* pc = reinterpret_cast<char*>(ia);
// Possibly in some other function
int* pi = reinterpret_cast<int*>(pc);

Хотя это может вызвать поведение undefined:

char* some_buffer; size_t offset; // Possibly passed in as an argument
int* pi = reinterpret_cast<int*>(some_buffer + offset);
pi[2] = -5;

Даже если мы можем гарантировать, что буфер достаточно велик, чтобы содержать три int s, выравнивание может быть неправильным. Как и во всех случаях поведения undefined, компилятор может делать абсолютно что угодно. Три общих момента могут быть:

  • В коде может работать Just Work (TM), потому что в вашей платформе выравнивание по умолчанию всех распределений памяти совпадает с выравниванием по умолчанию.
  • Показ указателя может округлить адрес до выравнивания int (что-то вроде pi = pc и -4), что потенциально может заставить вас читать/записывать в неправильную память.
  • Сама по себе разустановка указателя может потерпеть неудачу: процессор может отклонить несогласованные обращения, что приведет к сбою приложения.

Поскольку вы всегда хотите отразить UB, как сам дьявол, вам нужен массив char с правильным размером и выравниванием. Самый простой способ получить это - просто начать с массива "правильного" типа (int в этом случае), а затем заполнить его с помощью указателя char, который будет разрешен, поскольку int является типом POD.

Добавление: после использования размещения new вы сможете вызвать любую функцию на объекте. Если конструкция правильная и не вызывает UB из-за вышеизложенного, то вы успешно создали объект в нужном месте, поэтому любые вызовы в порядке, даже если объект был не-POD (например, поскольку он имел виртуальные функции). В конце концов, любой класс распределителя скорее всего, будет использовать размещение new для создания объектов в хранилище, которое они получают. Обратите внимание, что это обязательно обязательно, если вы используете размещение new; другие применения типа punning (например, наивная сериализация с fread/fwrite) могут привести к неполному или некорректному объекту, поскольку некоторые значения в объекте должны обрабатываться специально для поддержания инвариантов класса.

Ответ 2

По сути, объяснение стандартного правила относительно типа указателя, карающего строгим псевдонимом, не обязательно правильно или легко понять. Стандарт не упоминает "строгий псевдоним", и я считаю, что оригинальная стандартная формулировка легче понять и обосновать.

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

Как вы видите, вопрос об "двухсторонней улице" даже не применим.