WedX - журнал о программировании и компьютерных науках

Псевдоним подписи с использованием reinterpret_cast

Возьмите следующий код

#include <iostream>

void func() {
    int i = 2147483640;
    while (i < i + 1)
    {
        std::cerr << i << '\n';
        ++i;
    }

    return;
}

int main() {
    func(); 
}

Этот код явно неверен, так как цикл while может завершиться только в случае переполнения подписанного int i, которым является UB, и, следовательно, компилятор может, например, оптимизировать его до бесконечного цикла (который Clang делает на -O3) или выполнять другие виды фанк вещи. Теперь мой вопрос: из моего чтения стандарта C ++ типы, которые эквивалентны псевдониму может со знаком (т.е. указатели int* и unsigned* могут быть псевдонимом). Имеет ли следующее поведение undefined или нет, чтобы сделать некую фанковую подписанную упаковку?

#include <iostream>

static int safe_inc(int a)
{
    ++reinterpret_cast<unsigned&>(a);
    return a;
}

void func() {
    int i = 2147483640;
    while (i < safe_inc(i))
    {
        std::cerr << i << '\n';
        ++i;
    }

    return;
}

int main() {
    func(); 
}

Я пробовал приведенный выше код с Clang 8 и GCC 9 на -O3 с -Wall -Wextra -Wpedantic -O3 -fsanitize=address,undefined аргументами и не получил ошибок или предупреждений, и цикл завершился после переноса на INT_MIN.

cppreference.com сообщает мне, что

Псевдонимы типов

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

  • AliasedType - это (возможно, cv-квалифицированный) вариант DynamicType со знаком или без знака.

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


  • Поскольку вы повторно интерпретируете signed int как значение unsigned, код будет таким же правильным, как если бы вы использовали значение unsigned с самого начала (и приведение cerr вывод в подписанный). 24.05.2019
  • @LightnessRacesinOrbit: [basic.lval] / 11 устанавливает действительность этого доступа. Не хватает того, что поведение при изменении неподписанного / подписанного объекта посредством ссылки на его подписанную / неподписанную версию. Но я не вижу заявления, запрещающего это. 24.05.2019
  • @NicolBolas, поскольку стандарт не делает различий между правом и доступом для чтения, я не вижу причин, по которым мы должны относиться к записи с особым подозрением. 24.05.2019
  • @SergeyA: Затем укажите строку в спецификации, которая говорит, что на самом деле происходит, когда вы пишете в подписанный объект через ссылку на беззнаковый. Потому что я могу указать на строки в спецификации, в которых говорится, что происходит, например, когда вы вызываете функцию-член производного класса с помощью указателя / ссылки базового класса. Но для подписанных / неподписанных подобных утверждений не существует. Преобразование законное; доступ является законным, но что происходит просто не указано в спецификации. 24.05.2019
  • @NicolBolas, хорошо, я понимаю вашу точку зрения. Может быть, это занижено? 24.05.2019
  • Верно. Мне всегда не нравились такие вещи. Это одна из немногих областей стандарта, которая, кажется, предполагает использование битовой логики, близкой к металлической. 24.05.2019
  • @SergeyA: Может быть, он не указан? Да, это дефект в спецификации. И с изменением двух дополнений в C ++ 20, это может быть решено совершенно четко определенным способом. Для этого просто должна быть какая-то формулировка. 24.05.2019
  • @NicolBolas: Когда был написан Стандарт, авторы ожидали, что действия, которые могут быть полезны и поддерживаются некоторыми реализациями, но могут не иметь смысла во всех реализациях, будут по-прежнему поддерживаться в реализациях, где они имеют смысл, даже если Стандарт не поддерживает t обязать это. Если в Стандарте есть дефект, то это не отказ от предписания поведения, а скорее его неспособность указать, что оно никогда не предназначалось как исчерпывающее. 24.05.2019
  • @supercat: Если это не исчерпывающе, значит, это не стандарт. Именно поэтому в стандарте есть утверждения, что X приводит к UB. В нем четко указывается, что действительно, а что нет. Пробелов быть не должно. То, что, по вашему мнению, ожидали авторы, просто неверно. Стандарт никогда не работал и никогда не предназначался для работы так, как вы этого хотите. 24.05.2019
  • @NicolBolas: В опубликованном Обосновании Стандарта кодирования C говорится, что важной целью UB является предоставление рынку возможности решать, какие виды реализаций должны поддерживать какие популярные расширения. C89 был написан для описания базового языка, который реализации, предназначенные для различных целей, будут дополнять по мере необходимости для выполнения этих целей; ни он, ни какая-либо другая версия с тех пор никогда не были значимыми в качестве полного стандарта. Авторы открыто признают, что реализация может быть согласованной, но бесполезной, а определение соответствующей программы еще более расплывчато. 24.05.2019
  • @supercat: важная цель UB, которая не имеет отношения к вопросу о полноте. В полной спецификации такой случай должен быть определен как четко определенный, определяемый реализацией, неопределенный или неопределенный. Здесь мы говорим о неполной спецификации, где ничего не говорится о том, что происходит в этом случае. Сказать ничего не значит сказать, что что-то не определено. 24.05.2019
  • @NicolBolas: Считаете ли вы, что опубликованное Обоснование стандарта кодирования C не точно отражает намерение Комитета? Если да, то есть ли у вас какие-либо доказательства противного намерения Комитета? 24.05.2019
  • @supercat: Нет, я считаю, что вы применяете его в ситуации, к которой он не применим. Вы не понимаете разницы между спецификацией, явно объявляющей что-то как UB, и спецификацией, просто не упоминающей, что происходит. Когда, где и почему авторы спецификации решили использовать UB, не имеет отношения к обсуждению сценария, который не является неопределенным поведением. Это поведение, которое не описывается в спецификации. 24.05.2019
  • @NicolBolas: В языке, описанном K&R и K & R2, принцип, согласно которому объекты хранят свои значения как последовательность последовательных байтов по их адресу, может применяться транзитивно; этого достаточно, чтобы определить поведение многих конструкций, которые не нужно указывать индивидуально. Если в Стандарте сказано, что такие принципы применяются транзитивно за исключением реализаций, которые документируют веские причины для выполнения чего-то еще, это оставит неоднозначным вопрос о том, когда именно соответствующие реализации могут указывать странное поведение, но определять поведение этих это не так. 24.05.2019
  • Такой код является UB, поскольку нет объекта этого типа (unsigned); если бы он был, его жизнь не началась бы; и в любом случае он будет неинициализирован. 25.05.2019
  • Также см. Что такое строгое правило псевдонима, которое охватывает эту область более полно и в целом. 25.05.2019
  • Трансляторы @supercat C / C ++ не рассматривают многие конструкции как набор битов; символическая интерпретация полагается на то, что они не противоречат их репрезентации. 25.05.2019
  • @curiousguy: На языке, изобретенном Деннисом Ричи и описанном в книгах K&R, объекты представляли собой мешки с битами. Авторы Стандарта не требовали, чтобы реализации могли использоваться в качестве ассемблеров высокого уровня, но они явно заявили, что не хотят препятствовать использованию языка таким образом непереносимым кодом, поскольку они признали, что способность поддерживать такой код была одной из сильных сторон C. 25.05.2019
  • re занижение спецификации, это, по крайней мере, очень строго указано для bool 30.05.2019

Ответы:


1

Ваш код совершенно законен, ссылка cpp - очень хороший источник. Вы можете найти ту же информацию в стандартном [basic.lval] / 11

Если программа пытается получить доступ к сохраненному значению объекта через glvalue, тип которого не похож ([conv.qual]) на один из следующих типов, поведение не определено:

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

  • тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта, [...]

24.05.2019
  • Хм, что здесь означает соответствие? Конечно, это не та формулировка, которую Cubbi выбрал в cppreference (вариант). 24.05.2019
  • @LightnessRacesinOrbit eel.is/c++draft/basic.fundamental#2. Ссылка на Cpp предпочитает простой английский, цель, конечно же, сделать его проще для чтения, чем стандартный. 24.05.2019
  • Для полноты, поведение вашего кода более определено в соответствии со стандартом C ++ 20: eel.is/c++draft/basic.types#basic.fundamental-3. Тем не менее, этот добавленный абзац является просто признанием факта, и вы можете смело предполагать, что ваш код переносим. 24.05.2019
  • @curiousguy Это здесь просто, чтобы показать, что абзац ссылки cpp, указанный в вопросе, имеет эквивалент в стандарте. Если бы мне пришлось продемонстрировать, что приведенный выше фрагмент кода соответствует стандарту и выполняет то, что должен делать, мне пришлось бы разместить около 10% от стандарта! 25.05.2019
  • @curiousguy Это абзац, в котором используется псевдоним UB, за некоторыми исключениями. Вы можете попытаться найти другой абзац, в котором говорится о доступе к значению через другой тип ... чтобы убедить себя. 25.05.2019
  • @curiousguy Эквивалент абзац стандарта C: §6.5 / 7 25.05.2019
  • Так что же означает reinterpret_cast<unsigned&>(x)? Разве +reinterpret_cast<unsigned&>(x) не должно быть значением x? Где использование reinterpred_casted ptr / lvalue определяется как переинтерпретация представления объекта? 25.05.2019
  • В любой стандартной версии C ++, которую вы хотите: вы утверждаете, что каждый случай, явно разрешенный так называемым правилом псевдонима, действителен (не UB); в отношении: тип агрегата или объединения, который включает в себя один из вышеупомянутых типов среди своих элементов или нестатических элементов данных (включая, рекурсивно, элемент или нестатический член данных субагрегата или содержащегося объединения). Можете ли вы предоставить пример, в котором используется член (рекурсивно) агрегата? 25.05.2019
  • @curiousguy О, вы ссылаетесь на старую версию стандарта C ++. Это правило, которое вы создаете на сайте, пришло из C. В C это необходимо для определения того, что является доступом для членов. В C ++ он был удален, потому что в этом нет необходимости, доступ к члену определяется где-то еще в стандарте. См. Проблему с основным языком # 2051 25.05.2019
  • @Oliv Значит, std явно разрешал то, что на практике было невозможно? Разве это не влияет на (мета) аргумент о том, что, если он указан как не запрещенный, он должен быть допустимым и полезным? 25.05.2019
  • @curiousguy Интерпретация значения зависит от типа выражения, intro.object Для других объектов интерпретация найденных в них значений определяется типом выражений ([expr.compound]), используемых для доступа к ним. 25.05.2019
  • @curiousguy 6 месяцев назад я действительно задавал себе тот же вопрос. В чем смысл этого правила: stackoverflow.com/questions/53151521/. После нескольких вопросов и обсуждения в комментариях кто-то объяснил мне, что это всего лишь реликвия C, и в комитете по C ++ обсуждали именно это. Видимо решили убрать это правило. 25.05.2019
  • @Oliv Наконец-то вы можете попрощаться с грязной формулировкой. 16.04.2021
  • @LanguageLawyer Я убежден, что язык можно исправить, потому что на момент создания C и C ++ мы еще не обнаружили правильных абстракций для описания программ. Основной источник путаницы - невозможность рассматривать ссылочный тип как обычный тип. Во-вторых, следует учитывать, что тип - это свойство памяти, а не доступа. Это исправлено в ржавчине элегантно, с множеством других исправлений. Вам следует попробовать этот язык. Для меня C ++ принадлежит к наследию. 17.04.2021

  • 2

    Псевдонимы здесь совершенно законны. См. https://eel.is/c++draft/expr.prop#basic.lval-11.2:

    Если программа пытается получить доступ к сохраненному значению объекта через glvalue, тип которого не похож ([conv.qual]) на один из следующих типов, поведение не определено: 53

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

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

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

     unsigned x = i;
     ++x;
     i = x; // this would serve you just fine.
    

    Этот код будет определяться реализацией до C ++ 20, поскольку вы будете преобразовывать значение, которое не может быть представлено типом назначения.

    Начиная с C ++ 20 этот код будет правильно сформирован.

    См. https://en.cppreference.com/w/cpp/language/implicit_conversion

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

    24.05.2019
  • Интересно, нельзя ли сделать то же самое без reinterpret_cast via unsigned x = a; ++x; return x;. Разве это не имело бы такого же эффекта, но действовало бы уже до c ++ 20? 24.05.2019
  • @ previouslyknownas_463035818 спасибо за указание на это, reinterpret_cast - отвлекающий маневр. Но результатом все равно будет реализация, определенная до C ++ 20. 24.05.2019
  • Это не отвечает на вопрос о псевдонимах, хотя делает это. 24.05.2019
  • @NathanOliver, почему это должно быть определено в реализации? unsigned x = a; в порядке, пока a вписывается в unsigned, с unsigend приращение нормально, а затем unsigned снова входит в int 24.05.2019
  • @LightnessRacesinOrbit как вы думаете, вопрос в первую очередь о алиасинге? Я чувствую, что это может быть два вопроса? 24.05.2019
  • причина всего этого состоит в том, чтобы оставаться в подписанном пространстве как можно дольше (в частности, из-за того, что поведение undefined дает компилятору больше свободы в оптимизации кода) и делать безопасный откат к неподписанному только там, где это необходимо. 24.05.2019
  • На самом деле при отливке к эталону здесь могут быть подводные камни, в зависимости от того, что отлито. 24.05.2019
  • @ JonasMüller, ваш вопрос об инкременте или псевдониме? 24.05.2019
  • @ previouslyknownas_463035818, если a равно INT_MAX, тогда x не впишется обратно в a, делая его поведение определяемым реализацией. 24.05.2019
  • Я согласен, здесь есть второй неявный вопрос (который касается части переполнения), но основной вопрос заключается в том, допустимо ли здесь использование reinterpret_cast. 24.05.2019
  • OP спрашивает, хорошо ли определен его подход с использованием псевдонимов; кажется довольно ясным 24.05.2019
  • @NathanOliver, да ладно, детали кажутся более опасными, чем кажется на первый взгляд 24.05.2019
  • @LightnessRacesinOrbit также добавил часть псевдонима, это законно. 24.05.2019
  • Насколько я понимаю (не считая недооцененной модификации), это вызывает проблемы только на машинах со знаковой величиной, битовый шаблон INT_MAX + 1 имеет отрицательный ноль на знаковой величине, что может быть представлением ловушки. В дополнении до единиц и двух битовая комбинация INT_MAX + 1 не является представлением ловушки и поэтому не должна вызывать проблем. Или я что-то тут контролирую? 24.05.2019
  • C ++ - это абстракция. Вы должны представить, что на самом деле вы не программируете свой собственный кусок кремния, потому что это не так. Вы должны писать код в рамках правил абстрактной машины. Если вы этого не сделаете, вы можете столкнуться с огромными сложностями процесса перевода (и оптимизации). (Как вы ясно понимаете из своего комментария об оптимизации до бесконечного цикла!) 24.05.2019
  • @LightnessRacesinOrbit Я знаю, что вопрос в том, что помимо недооценки характера модификации, поднятой @ nicol-bolas, использование reinterpret_cast позволяет избежать вопроса о переполнении, потому что вы работаете непосредственно с битовым шаблоном и C ++ 17 и ниже задают дополнение до двух, дополнение до единиц и представления величины со знаком. Таким образом, я понимаю, что если полученное записанное значение не является представлением ловушки (и изменение должно быть четко определено), приращение является допустимым. 24.05.2019
  • Использование неправильного типа для доступа к объекту нигде не определено. Я даже не могу представить, как это можно определить неявно. 25.05.2019
  • @LightnessRacesinOrbit использование псевдонимов четко определено; кажется довольно четким Псевдоним (наличие двух разных путей для доступа к одному и тому же месту с разными типами) - это одно. Другой вариант - получить доступ к объекту с lvalue другого типа. 25.05.2019
  • @curiousguy Поможет, если вы не процитируете только половину моего предложения. Я сказал, что OP спрашивает, хорошо ли определен его подход с использованием псевдонима, а не просто использование псевдонима четко определено. Конечно, в подходе были и другие факторы, да. Однако сглаживание было большой частью этого. 25.05.2019
  • @LightnessRacesinOrbit Я указывал, что сглаживание различных типов указателей в целом не означает, что представление значения будет интерпретироваться как другой тип. Псевдонимы могут быть нормальными, в то время как переинтерпретация может иногда терпеть неудачу, если улавливание представления целых чисел со знаком было делом (что не на практике и в современном C ++, но было разрешено в какой-то момент AFAIK). 25.05.2019
  • @curiousguy И я согласен! 25.05.2019
  • Новые материалы

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

    Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
    Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..

    Учебные заметки: создание моего первого пакета Node.js
    Это мои обучающие заметки, когда я научился создавать свой самый первый пакет Node.js, распространяемый через npm. Оглавление Глоссарий I. Новый пакет 1.1 советы по инициализации..

    Забудьте о Matplotlib: улучшите визуализацию данных с помощью умопомрачительных функций Seaborn!
    Примечание. Эта запись в блоге предполагает базовое знакомство с Python и концепциями анализа данных. Привет, энтузиасты данных! Добро пожаловать в мой блог, где я расскажу о невероятных..

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


    Для любых предложений по сайту: wedx@cp9.ru