Разрешено сглаживание T * с помощью char *. Разрешено ли это другим способом? - программирование
Подтвердить что ты не робот

Разрешено сглаживание T * с помощью char *. Разрешено ли это другим способом?

Примечание. Этот вопрос был переименован и уменьшен, чтобы сделать его более сфокусированным и читаемым. Большинство комментариев относятся к старому тексту.


В соответствии со стандартом объекты другого типа могут не иметь одинаковое расположение памяти. Поэтому это не будет законным:

std::array<short, 4> shorts;
int* i = reinterpret_cast<int*>(shorts.data()); // Not OK

Однако стандарт допускает исключение из этого правила: к любому объекту можно получить доступ с помощью указателя на char или unsigned char:

int i = 0;
char * c = reinterpret_cast<char*>(&i); // OK

Однако мне непонятно, разрешено ли это другим способом. Например:

char * c = read_socket(...);
unsigned * u = reinterpret_cast<unsigned*>(c); // huh?
4b9b3361

Ответ 1

Некоторые из ваших кодов сомнительны из-за конверсий указателей. Имейте в виду, что в этих случаях reinterpret_cast<T*>(e) имеет семантику static_cast<T*>(static_cast<void*>(e)), поскольку используемые типы являются стандартными. (Я бы рекомендовал, чтобы вы всегда использовали static_cast через cv void* при работе с хранилищем.)

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

Алиасинг - это не конверсии указателей.. Это текст С++ 11, в котором описываются правила, которые обычно называются правилами "строгого сглаживания", от 3.10 Lvalues ​​и rvalues ​​[basic.lval ]:

10 Если программа пытается получить доступ к сохраненному значению объекта через значение gl другого, чем одно из следующих типов, поведение undefined:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) для динамического типа объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или неподписанный char тип.

(Это абзац 15 того же предложения и подпункта в С++ 03 с некоторыми незначительными изменениями в тексте с использованием, например, 'lvalue' вместо 'glvalue', поскольку последний является понятием С++ 11. )

В свете этих правил допустим, что реализация предоставляет нам magic_cast<T*>(p), которая каким-то образом преобразует указатель на другой тип указателя. Обычно это будет reinterpret_cast, что в некоторых случаях дает неуказанные результаты, но, как я уже объяснял, это не так для указателей на типы стандартного макета. Тогда верно, что все ваши фрагменты верны (подставляя reinterpret_cast с помощью magic_cast), потому что никакие glvalues ​​не связаны с результатами magic_cast.

Вот фрагмент, который, по-видимому, неправильно использует magic_cast, но я буду спорить правильно:

// assume constexpr max
constexpr auto alignment = max(alignof(int), alignof(short));
alignas(alignment) char c[sizeof(int)];
// I'm assuming here that the OP really meant to use &c and not c
// this is, however, inconsequential
auto p = magic_cast<int*>(&c);
*p = 42;
*magic_cast<short*>(p) = 42;

Чтобы оправдать мои рассуждения, предположите, что этот поверхностно другой фрагмент:

// alignment same as before
alignas(alignment) char c[sizeof(int)];

auto p = magic_cast<int*>(&c);
// end lifetime of c
c.~decltype(c)();
// reuse storage to construct new int object
new (&c) int;

*p = 42;

auto q = magic_cast<short*>(p);
// end lifetime of int object
p->~decltype(0)();
// reuse storage again
new (p) short;

*q = 42;

Этот фрагмент тщательно сконструирован. В частности, в new (&c) int; мне разрешено использовать &c, хотя c был уничтожен из-за правил, изложенных в параграфе 5 из 3.8 Object lifetime [basic.life]. В абзаце 6 из них приведены очень похожие правила для ссылок на хранилище, а в параграфе 7 объясняется, что происходит с переменными, указателями и ссылками, которые использовались для ссылки на объект после повторного использования его хранилища - я буду ссылаться на все вместе как на 3.8/5- 7.

В этом случае &c (неявно) преобразуется в void*, что является одним из правильного использования указателя на хранилище, которое еще не было повторно использовано. Аналогично p получается из &c до создания нового int. Его определение, возможно, может быть перенесено после уничтожения c, в зависимости от глубины магии реализации, но, конечно же, не после конструкции int: пункт 7 будет применяться, и это не одна из разрешенных ситуаций. Конструкция объекта short также полагается на p, становясь указателем на хранилище.

Теперь, поскольку int и short являются тривиальными типами, я не должен использовать явные вызовы деструкторам. Мне также не нужны явные вызовы конструкторам (то есть вызовы обычного, стандартного размещения new, объявленного в <new>). От 3.8 Время жизни объекта [basic.life]:

1 [...] Время жизни объекта типа T начинается, когда:

  • сохраняется хранилище с надлежащим выравниванием и размером для типа T и
  • Если объект имеет нетривиальную инициализацию, его инициализация завершена.

Время жизни объекта типа T заканчивается, когда:

  • если T - тип класса с нетривиальным деструктором (12.4), начинается вызов деструктора или
  • хранилище, которое объект занимает, повторно используется или освобождается.

Это означает, что я могу переписать код таким образом, что после складывания промежуточной переменной q я получаю исходный фрагмент.

Заметьте, что p не может быть сложен. Иными словами, следующее неверно неверно:

alignas(alignment) char c[sizeof(int)];
*magic_cast<int*>(&c) = 42;
*magic_cast<short*>(&c) = 42;

Если предположить, что объект int (тривиально) построен со второй строкой, то это означает, что &c становится указателем на хранилище, которое было повторно использовано. Таким образом, третья строка неверна - хотя из-за 3.8/5-7 и не из-за правил сглаживания строго говоря.

Если мы не предполагаем, что вторая строка является нарушением правил сглаживания: мы читаем то, что на самом деле является объектом char c[sizeof(int)], через glvalue типа int, который не является одним из разрешенных исключение. Для сравнения, *magic_cast<unsigned char>(&c) = 42; было бы точным (мы предположили бы, что объект short тривиально построен на третьей строке).

Как и в случае с Alf, я также рекомендую вам использовать стандартное размещение при использовании хранилища. Пропуск уничтожения для тривиальных типов прекрасен, но если вы встретите *some_magic_pointer = foo;, вы, вероятно, столкнетесь либо с нарушением 3.8/5-7 (независимо от того, насколько магически этот указатель был получен), либо правил сглаживания. Это означает также сохранение результата нового выражения, так как вы, скорее всего, не сможете повторно использовать волшебный указатель после создания объекта - из-за 3.8/5-7 снова.

Чтение байтов объекта (это означает использование char или unsigned char) в порядке, но вы даже не можете использовать reinterpret_cast или что-либо волшебство вообще. static_cast через cv void*, возможно, отлично подходит для работы (хотя мне кажется, что стандарт может использовать там лучшую формулировку).

Ответ 2

Это тоже:

// valid: char -> type
alignas(int) char c[sizeof(int)];
int * i = reinterpret_cast<int*>(c);

Это неверно. В правилах псевдонимов указано, при каких обстоятельствах законно/незаконно обращаться к объекту через lvalue другого типа. Существует определенное правило, в котором говорится, что вы можете получить доступ к любому объекту с помощью указателя типа char или unsigned char, поэтому первый случай правильный. То есть A = > B не обязательно означает B = > A. Вы можете получить доступ к int с помощью указателя на char, но вы не можете получить доступ к char с помощью указателя на int.


В интересах Alf:

Если программа пытается получить доступ к сохраненному значению объекта через значение gl другого, чем одно из следующих типов, поведение undefined:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) для динамического типа объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических элементов данных (включая рекурсивно, элемент или нестатический элемент данных субагрегата или содержащегося объединения),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или неподписанный char тип.

Ответ 3

Что касается действительности & hellip;

alignas(int) char c[sizeof(int)];
int * i = reinterpret_cast<int*>(c);

Сам reinterpret_cast в порядке или нет, в смысле создания полезного значения указателя, в зависимости от компилятора. И в этом примере результат не используется, в частности, массив символов не доступен. Таким образом, не так много можно сказать о примере as-is: это просто зависит.

Но рассмотрим расширенную версию, которая затрагивает правила псевдонимов:

void foo( char* );

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

foo( c );
int* p = reinterpret_cast<int*>( c );
cout << *p << endl;

И давайте рассмотрим только случай, когда компилятор гарантирует полезное значение указателя, которое поместит pointee в одни и те же байты памяти (причина, по которой это зависит от компилятора, заключается в том, что стандарт в §5.2.10/7, только гарантирует его для конверсий указателей, где типы совместимы с выравниванием, и в противном случае оставить его "неуказанным" (но тогда все из § 5.2.10 несколько противоречит § 9.2/18).

Теперь, одна интерпретация стандарта §3.10/10, так называемое предложение "строгое сглаживание" (но обратите внимание, что стандарт никогда не использует термин "строгий псевдоним" ),

Если программа пытается получить доступ к сохраненному значению объекта через значение gl другого, чем одно из следующих типов, поведение undefined:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) для динамического типа объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических элементов данных (включая рекурсивно, элемент или нестатический элемент данных субагрегата или содержащегося объединения),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или unsigned char.

заключается в том, что, как он сам говорит, относится к динамическому типу объекта, находящегося в байтах c.

С этой интерпретацией операция чтения в *p ОК, если foo разместил там объект int, а в противном случае - нет. Таким образом, в этом случае массив char получает доступ с помощью указателя int*. И никто не сомневается в том, что другой способ действителен: хотя foo мог разместить объект int в этих байтах, вы можете свободно получить доступ к этому объекту как последовательность значений char, последним типом §3.10/10.

Итак, с этой (обычной) интерпретацией, после того, как foo разместил там int, мы можем получить к ней доступ как объекты char, поэтому по крайней мере один объект char существует в области памяти с именем c; и мы можем получить к нему доступ как int, поэтому существует, по крайней мере, один int; и поэтому David & rsquo; утверждение s в другом ответе, что объекты char не могут быть доступны как int, несовместимы с этой обычной интерпретацией.

Утверждение Дэвида также несовместимо с наиболее распространенным использованием нового места размещения.

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

Итак, в заключение, что касается Священного стандарта, просто наведение указателя T* на массив практически полезно или не зависит от компилятора, и доступ к указанному значению может быть действительным или не в зависимости от того, что присутствует. В частности, подумайте о представлении ловушки int: вы не хотели бы, чтобы это взорвалось на вас, если битпаттерс оказался таким. Поэтому, чтобы быть в безопасности, вы должны знать, что там, биты, а как вызов foo выше иллюстрирует, что компилятор вообще не знает, что, например, g++-компилятор строгий оптимизатор на основе выравнивания может вообще не знать, что & hellip;