Действительно ли тип reinterpret_cast наказывает неопределенное поведение? - PullRequest
0 голосов
/ 01 января 2019

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


[expr.reinterpret.cast] / 11 говорится:

Выражение glvalue типа T1 может быть приведено к типу «ссылка на T2», если выражение типа «указатель на T1» может быть явно преобразовано в тип «указатель»до T2 ”, используя reinterpret_­cast.Результат ссылается на тот же объект, что и источник glvalue, но с указанным типом.[Примечание: то есть для l-значений эталонное приведение reinterpret_­cast<T&>(x) имеет тот же эффект, что и преобразование *reinterpret_­cast<T*>(&x) со встроенными операторами & и * (и аналогично для reinterpret_­cast<T&&>(x)).- примечание конца] Временное создание не производится, копирование не производится, конструкторы или функции преобразования не вызываются.

со сноской:

75) Это иногдаобозначаемый как тип pun .

/ 11, неявно, например, содержит ограничения от / 6 до / 10, но, возможно, наиболее распространенное использование (punning объекты ) адресуются в [expr.reinterpret.cast] / 7 :

Указатель объекта может быть явно преобразован в указатель объекта другого типа.Когда значение v типа указателя объекта преобразуется в тип указателя объекта «указатель на cv T», в результате получается static_­cast<cv T*>(static_­cast<cv void*>(v)).[Примечание: преобразование значения типа «указатель на T1» в тип «указатель на T2» (где T1 and T2 - это типы объектов и где требования к выравниванию T2 не являются более строгими, чем требования * 1051)*) и возврат к исходному типу возвращает исходное значение указателя.- примечание конца]

Очевидно, что целью не может быть преобразование в / из указателей или ссылки на void, так как:

  1. пример в / 7 ясно демонстрирует, что static_cast должно быть достаточно для указателей, как и [expr.static.cast] / 13 и [conv.ptr] / 2
  2. [преобразования в] ссылки на void являются недействительными prima facie .

Далее, [basic.lval] / 8 states:

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

(8.1)динамический тип объекта,

(8.2) cv-квалифицированная версия динамического типа объекта,

(8.3) тип, аналогичный динамическому типу объекта,

(8.4) тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта,

(8.5) тип, который является типом со знаком или без знака, соответствующим версии с квалификацией cvдинамического типа объекта,

(8.6) агрегатного типа или типа объединения, который включает один из вышеупомянутых типов среди своих элементов или элементов не статических данных (включая рекурсивно, элемент или нестатические данные)член субагрегата или автономного объединения),

(8.7) тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,

(8.8) char, unsigned char или std ::Тип байта.

И если мы вернемся к [expr.reinterpret.cast] / 11 на мгновение мы видим: «Результат относится к того же объекта , что и у источника glvalue, , но с указанным типом .»Это говорит мне как явное утверждение, что результатом reinterpret_cast<T&>(v) является lvalue ссылка на объект типа T, доступ к которому явно "через glvalue" "динамического типапредмет".В этом предложении также рассматривается аргумент, что различные параграфы [basic.life] применяются посредством ложного утверждения, что результаты таких преобразований относятся к новому объекту типа T, время жизни которого еще не былоНачало, которое просто находится на том же адресе памяти, что и v.

Кажется бессмысленным явное определение таких преобразований только для запрета стандартно определенного использования из результатов , особенно в свете сноски 75, отмечающей, что такое [эталонное] преобразование «иногда упоминается как type pun

Обратите внимание, что мои ссылкидо окончательного общедоступного проекта для C ++ 17 (N4659), но рассматриваемый язык мало изменился с N3337 (C ++ 11) до N4788 (C ++ 20WD) (ссылка на отзыв, вероятно, будет относиться к более поздним черновикам во времени).Фактически сноска к [expr.reinterpret.cast] / 11 сделана еще более явной в последнем проекте:

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

Ответы [ 3 ]

0 голосов
/ 03 января 2019

[basic.lval] / 8 говорит, что поведение наверняка будет неопределенным, но это не обязательно означает, что если вы сделаете что-то из списка в [basic.lval] / 8, поведение будет определено.

[basic.lval] / 8 не сильно изменился со времени C ++ 98 и имеет неточную формулировку, такую ​​как использование неопределенного термина «динамический тип объекта».(C ++ определяет динамические типы для выражений).

Определенность поведения в случае, если вы делаете что-то, разрешенное [basic.lval] / 8, зависит от других частей стандарта.Даже если можно согласиться с тем, что результат реинтерпретации со знаком / без знака может быть получен из формулировки в [basic.types], я не могу представить, как можно предсказать результат доступа к объекту, содержащему ссылки или виртуальные методычерез char glvalue.

Новые правила приведения указателей и glvalue в C ++ 17 сделали [basic.lval] / 8 более бесполезным, потому что теперь формально невозможно достичь поставленных целей [basic.lval] / 8гарантировать (например, прочитать байты в объекте через char glvalue).Как вы указали, в [expr.reinterpret.cast] / 7 после reinterpret_cast со ссылкой на T результирующее значение glvalue по-прежнему ссылается на объект, на который ссылается аргумент reinterpret_cast.

Per [conv.lval] / (3.4) , результатом преобразования lvalue-в-значение является значение, содержащееся в объекте, на который ссылается преобразованное glvalue.Например, эти правила означают, что результат преобразования lvalue в rvalue, примененный к reinterpret_cast<char&>(i), где i - это переменная int, - это значение, хранящееся в объекте i int.Тип значения: char ( [conv.lval] / 1 ) и, если значение i не может быть представлено char, согласно [expr] / 1 поведение не определено.Попытка чтения объекта int через char glvalue приведет к UB, если значение объекта не будет представлено char, даже если этот доступ «разрешен» с помощью [basic.lval] / (8.8).Это доказывает то, что было сказано в первом абзаце.

0 голосов
/ 08 января 2019

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

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

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

0 голосов
/ 01 января 2019

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

Это говорит о том, что результатом reinterpret_cast<T&>(v) является lvalue ссылка на объект типа T, к которому доступ явно "через glvalue" "динамический тип объекта".

[basic.lval] / 8 немного вводит в заблуждениеон говорит о динамическом типе «объекта», когда динамический тип фактически является свойством glvalue [defns.dynamic.type] , используемым для доступа к объекту, а не к самому объекту.По сути, динамический тип glvalue - это тип объекта, который в настоящее время проживает в том месте, на которое ссылается glvalue (фактически, тип объекта, который был создан / инициализирован в этом фрагменте памяти) [intro.object] / 6 .Например:

float my_float = 42.0f;
std::uint32_t& ui = reinterpret_cast<std::uint32_t&>(my_float);

здесь, ui является ссылкой, которая ссылается на объект, созданный по определению my_float.Однако доступ к этому объекту через glvalue ui вызовет неопределенное поведение (для [basic.lval] /8.1), поскольку динамический тип для glvalue равен float, а тип для glvalue равен std::uint32_t.

Существует несколько допустимых вариантов использования reinterpret_cast, подобных этому, но существуют случаи использования, отличные от простого приведения к void* и обратно (для последнего достаточно было бы static_cast, как вы сами отметили). [basic.lval] / 8 эффективно дает вам полный список того, что они есть.Например, было бы правильно проверить (и даже скопировать, если динамический тип объекта тривиально-копируемый [basic.types] / 9 ) значение объекта с помощьюприведение адреса объекта к char*, unsigned char* или std::byte8 (однако не signed char*).Было бы допустимо reinterpret_cast объект со знаком типа для доступа к нему как к соответствующему типу без знака и наоборот.Также было бы правильно привести указатель / ссылку на объединение к указателю / ссылке на член этого объединения и получить доступ к этому члену через результирующее значение lvalue , если этот член является активным членом объединения…

Основная причина, по которой определение типа с помощью приведений типа в общем случае не определено, заключается в том, что его определенное поведение запретило бы некоторые чрезвычайно важные оптимизации компилятора.Если вы позволите просто получить доступ к любому объекту любого типа через lvalue любого другого типа, то компилятор должен будет предположить, что любая модификация объекта через некоторое lvalue может потенциально повлиять на значение любого объекта в программе, если толькоэто может доказать обратное.В результате, в принципе, было бы невозможно, например, хранить содержимое в регистрах в течение любого полезного периода времени, потому что любая модификация чего-либо немедленно делала бы недействительным то, что вы можете иметь в регистрах в данный момент.Да, любой хороший оптимизатор будет выполнять анализ псевдонимов .Но, хотя такие методы, безусловно, работают и эффективны, они, в принципе, могут охватывать только подмножество случаев.Опровергнуть или доказать псевдонимы вообще невозможно (эквивалентно решению проблемы остановки, я думаю)…

...