Псевдоним T * с символом * разрешен. Это также разрешено наоборот?

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


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

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?

27

Решение

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

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

Псевдоним не о преобразованиях указателей. Это текст C ++ 11, в котором изложены правила, которые обычно называются правилами «строгого алиасинга», из 3.10 Lvalues ​​и rvalues ​​[basic.lval]:

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

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

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

В свете этих правил давайте предположим, что реализация предоставляет нам magic_cast<T*>(p) который «каким-то образом» преобразует указатель в другой тип указателя. Обычно это было бы быть reinterpret_cast, который в некоторых случаях дает неуказанные результаты, но, как я объяснил ранее, это не так для указателей на типы стандартной компоновки. Тогда совершенно верно, что все ваши фрагменты верны (заменяя reinterpret_cast с magic_cast), потому что никакие глюки не связаны вообще с результатами 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 Срок службы объекта [basic.life]. В параграфе 6 того же самого правила очень похожи на ссылки на хранилище, а в параграфе 7 объясняется, что происходит с переменными, указателями и ссылками, которые использовались для ссылки на объект после повторного использования его хранилища — я буду называть их как 3.8 / 5- 7.

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

Теперь, потому что int а также short тривиальные типы, мне не нужно использовать явные вызовы деструкторов. Мне также не нужны явные вызовы конструкторам (то есть вызовы обычного стандартного размещения, объявленного в <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* возможно, подходит для этой работы (хотя я чувствую, что Стандарт мог бы использовать там более качественную формулировку).

23

Другие решения

Это тоже:

// 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,


В интересах Альфа:

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

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

Что касается действительности …

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

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

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

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 стандарта, так называемое предложение «строгого алиасинга» (но обратите внимание, что в стандарте никогда не используется термин «строгое алиасинг»),

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

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

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

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

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

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

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

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

2
По вопросам рекламы [email protected]